Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 5 | | | |
bc77d7c0 TO |
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 | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
cdfa6649 | 11 | |
12 | use Civi\Api4\Contact; | |
34f3f22a | 13 | use Civi\Api4\RelationshipType; |
018c9e26 | 14 | use Civi\Api4\StateProvince; |
cdfa6649 | 15 | |
10438107 | 16 | require_once 'api/v3/utils.php'; |
17 | ||
6a488035 TO |
18 | /** |
19 | * | |
20 | * @package CRM | |
ca5cec67 | 21 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
22 | */ |
23 | ||
6a488035 TO |
24 | /** |
25 | * class to parse contact csv files | |
26 | */ | |
68f3bda5 | 27 | class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser { |
91b4c63e | 28 | |
29 | use CRM_Contact_Import_MetadataTrait; | |
30 | ||
44edf1fc | 31 | protected $_mapperKeys = []; |
6a488035 | 32 | |
186628c3 | 33 | /** |
34 | * Is update only permitted on an id match. | |
35 | * | |
36 | * Note this historically was true for when id or external identifier was | |
37 | * present. However, CRM-17275 determined that a dedupe-match could over-ride | |
38 | * external identifier. | |
39 | * | |
40 | * @var bool | |
41 | */ | |
6a488035 TO |
42 | protected $_updateWithId; |
43 | protected $_retCode; | |
44 | ||
45 | protected $_externalIdentifierIndex; | |
5ebaab5d | 46 | protected $_allExternalIdentifiers = []; |
6a488035 TO |
47 | protected $_parseStreetAddress; |
48 | ||
49 | /** | |
ceb10dc7 | 50 | * Array of successfully imported contact id's |
6a488035 | 51 | * |
69078420 | 52 | * @var array |
6a488035 TO |
53 | */ |
54 | protected $_newContacts; | |
55 | ||
56 | /** | |
fe482240 | 57 | * Line count id. |
6a488035 TO |
58 | * |
59 | * @var int | |
60 | */ | |
61 | protected $_lineCount; | |
62 | ||
63 | /** | |
ceb10dc7 | 64 | * Array of successfully imported related contact id's |
6a488035 | 65 | * |
69078420 | 66 | * @var array |
6a488035 TO |
67 | */ |
68 | protected $_newRelatedContacts; | |
69 | ||
68f3bda5 EM |
70 | protected $_tableName; |
71 | ||
72 | /** | |
73 | * Total number of lines in file | |
74 | * | |
75 | * @var int | |
76 | */ | |
77 | protected $_rowCount; | |
78 | ||
68f3bda5 EM |
79 | protected $_primaryKeyName; |
80 | protected $_statusFieldName; | |
81 | ||
82 | protected $fieldMetadata = []; | |
34f3f22a | 83 | |
92414c61 EM |
84 | /** |
85 | * Fields which are being handled by metadata formatting & validation functions. | |
86 | * | |
87 | * This is intended as a temporary parameter as we phase in metadata handling. | |
88 | * | |
89 | * The end result is that all fields will be & this will go but for now it is | |
90 | * opt in. | |
91 | * | |
b7d52f5e | 92 | * @var string[] |
92414c61 EM |
93 | */ |
94 | protected $metadataHandledFields = [ | |
e0b8f9a9 EM |
95 | 'contact_type', |
96 | 'contact_sub_type', | |
b7d52f5e EM |
97 | 'gender_id', |
98 | 'birth_date', | |
99 | 'deceased_date', | |
100 | 'is_deceased', | |
c0010ab5 EM |
101 | 'prefix_id', |
102 | 'suffix_id', | |
103 | 'communication_style', | |
104 | 'preferred_language', | |
02374bae | 105 | 'preferred_communication_method', |
639e4f37 EM |
106 | 'phone', |
107 | 'im', | |
108 | 'openid', | |
109 | 'email', | |
110 | 'website', | |
111 | 'url', | |
80e9f1a2 EM |
112 | 'email_greeting', |
113 | 'email_greeting_id', | |
114 | 'postal_greeting', | |
115 | 'postal_greeting_id', | |
116 | 'addressee', | |
117 | 'addressee_id', | |
424df54f EM |
118 | 'geo_code_1', |
119 | 'geo_code_2', | |
92414c61 EM |
120 | ]; |
121 | ||
34f3f22a EM |
122 | /** |
123 | * Relationship labels. | |
124 | * | |
125 | * Temporary cache of labels to reduce queries in getRelationshipLabels. | |
126 | * | |
127 | * @var array | |
128 | * e.g ['5a_b' => 'Employer', '5b_a' => 'Employee'] | |
129 | */ | |
130 | protected $relationshipLabels = []; | |
131 | ||
68f3bda5 EM |
132 | /** |
133 | * On duplicate | |
134 | * | |
135 | * @var int | |
136 | */ | |
137 | public $_onDuplicate; | |
138 | ||
139 | /** | |
140 | * Dedupe rule group id to use if set | |
141 | * | |
142 | * @var int | |
143 | */ | |
144 | public $_dedupeRuleGroupID = NULL; | |
145 | ||
6a488035 | 146 | /** |
fe482240 | 147 | * Class constructor. |
54957108 | 148 | * |
149 | * @param array $mapperKeys | |
6a488035 | 150 | */ |
7d2012dc | 151 | public function __construct($mapperKeys = []) { |
6a488035 | 152 | parent::__construct(); |
0b0285b1 | 153 | $this->_mapperKeys = $mapperKeys; |
6a488035 TO |
154 | } |
155 | ||
156 | /** | |
186628c3 | 157 | * The initializer code, called before processing. |
6a488035 | 158 | */ |
00be9182 | 159 | public function init() { |
52bd01f5 EM |
160 | $this->setFieldMetadata(); |
161 | foreach ($this->getImportableFieldsMetadata() as $name => $field) { | |
162 | $this->addField($name, $field['title'], CRM_Utils_Array::value('type', $field), CRM_Utils_Array::value('headerPattern', $field), CRM_Utils_Array::value('dataPattern', $field), CRM_Utils_Array::value('hasLocationType', $field)); | |
163 | } | |
be2fb01f | 164 | $this->_newContacts = []; |
6a488035 TO |
165 | |
166 | $this->setActiveFields($this->_mapperKeys); | |
6a488035 | 167 | |
6a488035 TO |
168 | $this->_externalIdentifierIndex = -1; |
169 | ||
170 | $index = 0; | |
171 | foreach ($this->_mapperKeys as $key) { | |
6a488035 TO |
172 | if ($key == 'external_identifier') { |
173 | $this->_externalIdentifierIndex = $index; | |
6a488035 TO |
174 | } |
175 | $index++; | |
176 | } | |
177 | ||
178 | $this->_updateWithId = FALSE; | |
7e56b830 | 179 | if (in_array('id', $this->_mapperKeys) || ($this->_externalIdentifierIndex >= 0 && $this->isUpdateExistingContacts())) { |
6a488035 TO |
180 | $this->_updateWithId = TRUE; |
181 | } | |
182 | ||
183 | $this->_parseStreetAddress = CRM_Utils_Array::value('street_address_parsing', CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'address_options'), FALSE); | |
184 | } | |
185 | ||
7e56b830 EM |
186 | /** |
187 | * Is this a case where the user has opted to update existing contacts. | |
188 | * | |
189 | * @return bool | |
190 | * | |
191 | * @throws \API_Exception | |
192 | */ | |
193 | private function isUpdateExistingContacts(): bool { | |
194 | return in_array((int) $this->getSubmittedValue('onDuplicate'), [ | |
195 | CRM_Import_Parser::DUPLICATE_UPDATE, | |
196 | CRM_Import_Parser::DUPLICATE_FILL, | |
197 | ], TRUE); | |
198 | } | |
199 | ||
64623d6c EM |
200 | /** |
201 | * Did the user specify duplicates checking should be skipped, resulting in possible duplicate contacts. | |
202 | * | |
203 | * Note we still need to check for external_identifier as it will hard-fail | |
204 | * if we duplicate. | |
205 | * | |
206 | * @return bool | |
207 | * | |
208 | * @throws \API_Exception | |
209 | */ | |
210 | private function isIgnoreDuplicates(): bool { | |
211 | return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser::DUPLICATE_NOCHECK; | |
212 | } | |
213 | ||
6a488035 | 214 | /** |
fe482240 | 215 | * Handle the values in preview mode. |
6a488035 | 216 | * |
13575591 EM |
217 | * Function will be deprecated in favour of validateValues. |
218 | * | |
77c5b619 TO |
219 | * @param array $values |
220 | * The array of values belonging to this line. | |
6a488035 | 221 | * |
7c550ca0 | 222 | * @return bool |
a6c01b45 | 223 | * the result of this processing |
06ef1cdc | 224 | * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID |
6a488035 | 225 | */ |
00be9182 | 226 | public function preview(&$values) { |
6a488035 TO |
227 | return $this->summary($values); |
228 | } | |
229 | ||
230 | /** | |
fe482240 | 231 | * Handle the values in summary mode. |
6a488035 | 232 | * |
13575591 EM |
233 | * Function will be deprecated in favour of validateValues. |
234 | * | |
77c5b619 TO |
235 | * @param array $values |
236 | * The array of values belonging to this line. | |
6a488035 | 237 | * |
b0f7df9b | 238 | * @return int |
a6c01b45 | 239 | * the result of this processing |
06ef1cdc | 240 | * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID |
6a488035 | 241 | */ |
ca275c67 | 242 | public function summary(&$values): int { |
2940ddb3 | 243 | $rowNumber = (int) ($values[array_key_last($values)]); |
b0f7df9b | 244 | try { |
13575591 | 245 | $this->validateValues($values); |
d4c8a770 | 246 | } |
b0f7df9b EM |
247 | catch (CRM_Core_Exception $e) { |
248 | $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage()); | |
249 | array_unshift($values, $e->getMessage()); | |
a05662ef | 250 | return CRM_Import_Parser::ERROR; |
6a488035 | 251 | } |
ca275c67 | 252 | $this->setImportStatus($rowNumber, 'NEW', ''); |
6a488035 | 253 | |
a05662ef | 254 | return CRM_Import_Parser::VALID; |
6a488035 TO |
255 | } |
256 | ||
755a3281 AP |
257 | /** |
258 | * Get Array of all the fields that could potentially be part | |
259 | * import process | |
260 | * | |
261 | * @return array | |
262 | */ | |
263 | public function getAllFields() { | |
264 | return $this->_fields; | |
265 | } | |
266 | ||
6a488035 | 267 | /** |
fe482240 | 268 | * Handle the values in import mode. |
6a488035 | 269 | * |
77c5b619 TO |
270 | * @param int $onDuplicate |
271 | * The code for what action to take on duplicates. | |
272 | * @param array $values | |
273 | * The array of values belonging to this line. | |
6a488035 | 274 | * |
7c550ca0 | 275 | * @return bool |
a6c01b45 | 276 | * the result of this processing |
a0c6165f | 277 | * |
278 | * @throws \CiviCRM_API3_Exception | |
279 | * @throws \CRM_Core_Exception | |
cdfa6649 | 280 | * @throws \API_Exception |
6a488035 | 281 | */ |
dba9288a | 282 | public function import($onDuplicate, &$values) { |
2940ddb3 | 283 | $rowNumber = (int) $values[array_key_last($values)]; |
be2fb01f | 284 | $this->_unparsedStreetAddressContacts = []; |
dba9288a | 285 | if (!$this->getSubmittedValue('doGeocodeAddress')) { |
6a488035 | 286 | // CRM-5854, reset the geocode method to null to prevent geocoding |
94d2b28e | 287 | CRM_Utils_GeocodeProvider::disableForSession(); |
6a488035 TO |
288 | } |
289 | ||
290 | // first make sure this is a valid line | |
291 | //$this->_updateWithId = false; | |
292 | $response = $this->summary($values); | |
6a488035 | 293 | |
a05662ef | 294 | if ($response != CRM_Import_Parser::VALID) { |
12d121eb | 295 | $this->setImportStatus((int) $values[count($values) - 1], 'Invalid', "Invalid (Error Code: $response)"); |
2940ddb3 | 296 | return FALSE; |
6a488035 TO |
297 | } |
298 | ||
5ebaab5d | 299 | $params = $this->getMappedRow($values); |
80e9f1a2 EM |
300 | $formatted = []; |
301 | foreach ($params as $key => $value) { | |
302 | if ($value !== '') { | |
303 | $formatted[$key] = $value; | |
639e4f37 EM |
304 | } |
305 | } | |
6a488035 | 306 | |
a0c6165f | 307 | $contactFields = CRM_Contact_DAO_Contact::import(); |
6a488035 | 308 | |
b6c2df03 | 309 | $params['contact_sub_type'] = $this->getContactSubType() ?: ($params['contact_sub_type'] ?? NULL); |
6a488035 | 310 | |
64623d6c | 311 | try { |
77b23b82 | 312 | [$formatted, $params] = $this->processContact($params, $formatted); |
64623d6c EM |
313 | } |
314 | catch (CRM_Core_Exception $e) { | |
315 | $statuses = [CRM_Import_Parser::DUPLICATE => 'DUPLICATE', CRM_Import_Parser::ERROR => 'ERROR', CRM_Import_Parser::NO_MATCH => 'invalid_no_match']; | |
2940ddb3 | 316 | $this->setImportStatus($rowNumber, $statuses[$e->getErrorCode()], $e->getMessage()); |
64623d6c EM |
317 | return FALSE; |
318 | } | |
e0b8f9a9 | 319 | |
65070890 | 320 | // Get contact id to format common data in update/fill mode, |
321 | // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id. | |
4bbc12e2 | 322 | |
6a488035 TO |
323 | //format common data, CRM-4062 |
324 | $this->formatCommonData($params, $formatted, $contactFields); | |
325 | ||
6a488035 TO |
326 | //fixed CRM-4148 |
327 | //now we create new contact in update/fill mode also. | |
4cc10497 | 328 | $contactID = NULL; |
60110bfa EM |
329 | //CRM-4430, don't carry if not submitted. |
330 | if ($this->_updateWithId && !empty($params['id'])) { | |
331 | $contactID = $params['id']; | |
6a488035 | 332 | } |
60110bfa | 333 | $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID); |
6a488035 | 334 | |
60110bfa | 335 | if (is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact)) { |
6a488035 TO |
336 | $newContact = clone($newContact); |
337 | $contactID = $newContact->id; | |
338 | $this->_newContacts[] = $contactID; | |
339 | ||
340 | //get return code if we create new contact in update mode, CRM-4148 | |
341 | if ($this->_updateWithId) { | |
a05662ef | 342 | $this->_retCode = CRM_Import_Parser::VALID; |
6a488035 TO |
343 | } |
344 | } | |
8fbfda98 | 345 | elseif (is_array($newContact)) { |
6a488035 | 346 | // if duplicate, no need of further processing |
a05662ef | 347 | if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) { |
2940ddb3 EM |
348 | $this->setImportStatus($rowNumber, 'DUPLICATE', 'Skipping duplicate record'); |
349 | return FALSE; | |
6a488035 TO |
350 | } |
351 | ||
f8d346cf BS |
352 | // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly |
353 | if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) { | |
354 | $dupeContactIDs = explode(',', $dupeContactIDs); | |
355 | } | |
6a488035 TO |
356 | $dupeCount = count($dupeContactIDs); |
357 | $contactID = array_pop($dupeContactIDs); | |
358 | // check to see if we had more than one duplicate contact id. | |
359 | // if we have more than one, the record will be rejected below | |
360 | if ($dupeCount == 1) { | |
361 | // there was only one dupe, we will continue normally... | |
362 | if (!in_array($contactID, $this->_newContacts)) { | |
363 | $this->_newContacts[] = $contactID; | |
364 | } | |
365 | } | |
366 | } | |
367 | ||
368 | if ($contactID) { | |
369 | // call import hook | |
370 | $currentImportID = end($values); | |
371 | ||
be2fb01f | 372 | $hookParams = [ |
6a488035 TO |
373 | 'contactID' => $contactID, |
374 | 'importID' => $currentImportID, | |
375 | 'importTempTable' => $this->_tableName, | |
376 | 'fieldHeaders' => $this->_mapperKeys, | |
377 | 'fields' => $this->_activeFields, | |
be2fb01f | 378 | ]; |
6a488035 TO |
379 | |
380 | CRM_Utils_Hook::import('Contact', 'process', $this, $hookParams); | |
381 | } | |
382 | ||
60110bfa | 383 | $primaryContactId = NULL; |
8fbfda98 | 384 | if (is_array($newContact)) { |
60110bfa EM |
385 | if ($dupeCount == 1 && CRM_Utils_Rule::integer($contactID)) { |
386 | $primaryContactId = $contactID; | |
6a488035 | 387 | } |
60110bfa EM |
388 | } |
389 | else { | |
390 | $primaryContactId = $newContact->id; | |
391 | } | |
6a488035 | 392 | |
8fbfda98 | 393 | if ((is_array($newContact) || is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) { |
6a488035 | 394 | |
60110bfa EM |
395 | //relationship contact insert |
396 | foreach ($this->getRelatedContactsParams($params) as $key => $field) { | |
397 | $formatting = $field; | |
398 | try { | |
399 | [$formatting, $field] = $this->processContact($field, $formatting); | |
400 | } | |
401 | catch (CRM_Core_Exception $e) { | |
402 | $statuses = [CRM_Import_Parser::DUPLICATE => 'DUPLICATE', CRM_Import_Parser::ERROR => 'ERROR', CRM_Import_Parser::NO_MATCH => 'invalid_no_match']; | |
403 | $this->setImportStatus((int) $values[count($values) - 1], $statuses[$e->getErrorCode()], $e->getMessage()); | |
404 | return FALSE; | |
405 | } | |
6a488035 | 406 | |
60110bfa | 407 | $contactFields = CRM_Contact_DAO_Contact::import(); |
6a488035 | 408 | |
60110bfa EM |
409 | //format common data, CRM-4062 |
410 | $this->formatCommonData($field, $formatting, $contactFields); | |
6a488035 | 411 | |
60110bfa EM |
412 | //fixed for CRM-4148 |
413 | if (!empty($params[$key]['id'])) { | |
414 | $contact = [ | |
415 | 'contact_id' => $params[$key]['id'], | |
416 | ]; | |
417 | $defaults = []; | |
418 | $relatedNewContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults); | |
419 | } | |
420 | else { | |
421 | $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE); | |
422 | } | |
423 | ||
424 | if (is_object($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) { | |
425 | $relatedNewContact = clone($relatedNewContact); | |
426 | } | |
427 | ||
428 | $matchedIDs = []; | |
429 | // To update/fill contact, get the matching contact Ids if duplicate contact found | |
430 | // otherwise get contact Id from object of related contact | |
eaedbd91 | 431 | if (is_array($relatedNewContact)) { |
8fbfda98 EM |
432 | $matchedIDs = $relatedNewContact['error_message']['params'][0]; |
433 | if (!is_array($matchedIDs)) { | |
434 | $matchedIDs = explode(',', $matchedIDs); | |
6a488035 | 435 | } |
60110bfa EM |
436 | } |
437 | else { | |
438 | $matchedIDs[] = $relatedNewContact->id; | |
439 | } | |
440 | // update/fill related contact after getting matching Contact Ids, CRM-4424 | |
441 | if (in_array($onDuplicate, [ | |
442 | CRM_Import_Parser::DUPLICATE_UPDATE, | |
443 | CRM_Import_Parser::DUPLICATE_FILL, | |
444 | ])) { | |
445 | //validation of related contact subtype for update mode | |
446 | //CRM-5125 | |
447 | $relatedCsType = NULL; | |
448 | if (!empty($formatting['contact_sub_type'])) { | |
449 | $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type'); | |
6a488035 TO |
450 | } |
451 | ||
60110bfa | 452 | if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))) { |
2940ddb3 EM |
453 | $this->setImportStatus((int) $values[count($values) - 1], 'invalid_no_match', 'Mismatched or Invalid contact subtype found for this related contact.'); |
454 | return FALSE; | |
6a488035 TO |
455 | } |
456 | else { | |
8fbfda98 | 457 | $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]); |
6a488035 | 458 | } |
60110bfa EM |
459 | } |
460 | static $relativeContact = []; | |
8fbfda98 | 461 | if (is_array($relatedNewContact)) { |
60110bfa EM |
462 | if (count($matchedIDs) >= 1) { |
463 | $relContactId = $matchedIDs[0]; | |
464 | //add relative contact to count during update & fill mode. | |
465 | //logic to make count distinct by contact id. | |
466 | if ($this->_newRelatedContacts || !empty($relativeContact)) { | |
467 | $reContact = array_keys($relativeContact, $relContactId); | |
468 | ||
469 | if (empty($reContact)) { | |
470 | $this->_newRelatedContacts[] = $relativeContact[] = $relContactId; | |
471 | } | |
6a488035 TO |
472 | } |
473 | else { | |
60110bfa | 474 | $this->_newRelatedContacts[] = $relativeContact[] = $relContactId; |
6a488035 TO |
475 | } |
476 | } | |
60110bfa EM |
477 | } |
478 | else { | |
479 | $relContactId = $relatedNewContact->id; | |
480 | $this->_newRelatedContacts[] = $relativeContact[] = $relContactId; | |
481 | } | |
482 | ||
8fbfda98 | 483 | if (is_array($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) { |
60110bfa EM |
484 | //fix for CRM-1993.Checks for duplicate related contacts |
485 | if (count($matchedIDs) >= 1) { | |
486 | //if more than one duplicate contact | |
487 | //found, create relationship with first contact | |
488 | // now create the relationship record | |
489 | $relationParams = [ | |
490 | 'relationship_type_id' => $key, | |
491 | 'contact_check' => [ | |
492 | $relContactId => 1, | |
493 | ], | |
494 | 'is_active' => 1, | |
495 | 'skipRecentView' => TRUE, | |
496 | ]; | |
497 | ||
498 | // we only handle related contact success, we ignore failures for now | |
499 | // at some point wold be nice to have related counts as separate | |
500 | $relationIds = [ | |
501 | 'contact' => $primaryContactId, | |
502 | ]; | |
503 | ||
504 | [$valid, $duplicate] = self::legacyCreateMultiple($relationParams, $relationIds); | |
505 | ||
506 | if ($valid || $duplicate) { | |
507 | $relationIds['contactTarget'] = $relContactId; | |
508 | $action = ($duplicate) ? CRM_Core_Action::UPDATE : CRM_Core_Action::ADD; | |
509 | CRM_Contact_BAO_Relationship::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action); | |
6a488035 | 510 | } |
6a488035 | 511 | |
60110bfa EM |
512 | //handle current employer, CRM-3532 |
513 | if ($valid) { | |
514 | $allRelationships = CRM_Core_PseudoConstant::relationshipType('name'); | |
515 | $relationshipTypeId = str_replace([ | |
516 | '_a_b', | |
517 | '_b_a', | |
518 | ], [ | |
519 | '', | |
520 | '', | |
521 | ], $key); | |
522 | $relationshipType = str_replace($relationshipTypeId . '_', '', $key); | |
523 | $orgId = $individualId = NULL; | |
524 | if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') { | |
525 | $orgId = $relContactId; | |
526 | $individualId = $primaryContactId; | |
6a488035 | 527 | } |
60110bfa EM |
528 | elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') { |
529 | $orgId = $primaryContactId; | |
530 | $individualId = $relContactId; | |
531 | } | |
532 | if ($orgId && $individualId) { | |
533 | $currentEmpParams[$individualId] = $orgId; | |
534 | CRM_Contact_BAO_Contact_Utils::setCurrentEmployer($currentEmpParams); | |
6a488035 TO |
535 | } |
536 | } | |
537 | } | |
538 | } | |
539 | } | |
540 | } | |
541 | if ($this->_updateWithId) { | |
542 | //return warning if street address is unparsed, CRM-5886 | |
57d6aced | 543 | return $this->processMessage($values, $this->_retCode); |
6a488035 TO |
544 | } |
545 | //dupe checking | |
eaedbd91 | 546 | if (is_array($newContact)) { |
085927a8 | 547 | return $this->handleDuplicateError($newContact, $values, $onDuplicate, $formatted, $contactFields); |
6a488035 | 548 | } |
5e21b588 EM |
549 | |
550 | if (empty($this->_unparsedStreetAddressContacts)) { | |
551 | $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '', $contactID); | |
552 | return CRM_Import_Parser::VALID; | |
553 | } | |
554 | ||
555 | // @todo - record unparsed address as 'imported' but the presence of a message is meaningful? | |
57d6aced | 556 | return $this->processMessage($values, CRM_Import_Parser::VALID); |
6a488035 TO |
557 | } |
558 | ||
3b9a6946 EM |
559 | /** |
560 | * Only called from import now... plus one place outside of core & tests. | |
561 | * | |
562 | * @todo - deprecate more aggressively - will involve copying to the import | |
563 | * class, adding a deprecation notice here & removing from tests. | |
564 | * | |
565 | * Takes an associative array and creates a relationship object. | |
566 | * | |
567 | * @deprecated For single creates use the api instead (it's tested). | |
568 | * For multiple a new variant of this function needs to be written and migrated to as this is a bit | |
569 | * nasty | |
570 | * | |
571 | * @param array $params | |
572 | * (reference ) an assoc array of name/value pairs. | |
573 | * @param array $ids | |
574 | * The array that holds all the db ids. | |
575 | * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer | |
576 | * "we are moving away from the $ids param " | |
577 | * | |
578 | * @return array | |
579 | * @throws \CRM_Core_Exception | |
580 | */ | |
581 | private static function legacyCreateMultiple($params, $ids = []) { | |
582 | // clarify that the only key ever pass in the ids array is 'contact' | |
583 | // There is legacy handling for other keys but a universe search on | |
584 | // calls to this function (not supported to be called from outside core) | |
585 | // only returns 2 calls - one in CRM_Contact_Import_Parser_Contact | |
586 | // and the other in jma grant applications (CRM_Grant_Form_Grant_Confirm) | |
587 | // both only pass in contact as a key here. | |
588 | $contactID = $ids['contact']; | |
589 | unset($ids); | |
590 | // There is only ever one value passed in from the 2 places above that call | |
591 | // this - by clarifying here like this we can cleanup within this | |
592 | // function without having to do more universe searches. | |
593 | $relatedContactID = key($params['contact_check']); | |
594 | ||
595 | // check if the relationship is valid between contacts. | |
596 | // step 1: check if the relationship is valid if not valid skip and keep the count | |
597 | // step 2: check the if two contacts already have a relationship if yes skip and keep the count | |
598 | // step 3: if valid relationship then add the relation and keep the count | |
599 | ||
600 | // step 1 | |
601 | [$contactFields['relationship_type_id'], $firstLetter, $secondLetter] = explode('_', $params['relationship_type_id']); | |
602 | $contactFields['contact_id_' . $firstLetter] = $contactID; | |
603 | $contactFields['contact_id_' . $secondLetter] = $relatedContactID; | |
604 | if (!CRM_Contact_BAO_Relationship::checkRelationshipType($contactFields['contact_id_a'], $contactFields['contact_id_b'], | |
605 | $contactFields['relationship_type_id'])) { | |
606 | return [0, 0]; | |
607 | } | |
608 | ||
609 | if ( | |
610 | CRM_Contact_BAO_Relationship::checkDuplicateRelationship( | |
611 | $contactFields, | |
612 | $contactID, | |
613 | // step 2 | |
614 | $relatedContactID | |
615 | ) | |
616 | ) { | |
617 | return [0, 1]; | |
618 | } | |
619 | ||
620 | $singleInstanceParams = array_merge($params, $contactFields); | |
621 | CRM_Contact_BAO_Relationship::add($singleInstanceParams); | |
622 | return [1, 0]; | |
623 | } | |
624 | ||
54847287 EM |
625 | /** |
626 | * Format common params data to proper format to store. | |
627 | * | |
628 | * @param array $params | |
629 | * Contain record values. | |
630 | * @param array $formatted | |
631 | * Array of formatted data. | |
632 | * @param array $contactFields | |
633 | * Contact DAO fields. | |
634 | */ | |
635 | private function formatCommonData($params, &$formatted, $contactFields) { | |
e0b8f9a9 | 636 | $customFields = CRM_Core_BAO_CustomField::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ?? NULL); |
54847287 EM |
637 | |
638 | $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address'); | |
639 | $customFields = $customFields + $addressCustomFields; | |
640 | ||
54847287 EM |
641 | //format date first |
642 | $session = CRM_Core_Session::singleton(); | |
643 | $dateType = $session->get("dateTypes"); | |
644 | foreach ($params as $key => $val) { | |
645 | $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key); | |
646 | if ($customFieldID && | |
647 | !array_key_exists($customFieldID, $addressCustomFields) | |
648 | ) { | |
649 | //we should not update Date to null, CRM-4062 | |
650 | if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) { | |
651 | //CRM-21267 | |
652 | CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key); | |
653 | } | |
654 | elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') { | |
655 | if (empty($val) && !is_numeric($val) && $this->_onDuplicate == CRM_Import_Parser::DUPLICATE_FILL) { | |
656 | //retain earlier value when Import mode is `Fill` | |
657 | unset($params[$key]); | |
658 | } | |
659 | else { | |
660 | $params[$key] = CRM_Utils_String::strtoboolstr($val); | |
661 | } | |
662 | } | |
663 | } | |
54847287 | 664 | } |
018c9e26 | 665 | $metadataBlocks = ['phone', 'im', 'openid', 'email', 'address']; |
639e4f37 EM |
666 | foreach ($metadataBlocks as $block) { |
667 | foreach ($formatted[$block] ?? [] as $blockKey => $blockValues) { | |
668 | if ($blockValues['location_type_id'] === 'Primary') { | |
669 | $this->fillPrimary($formatted[$block][$blockKey], $blockValues, $block, $formatted['id'] ?? NULL); | |
670 | } | |
671 | } | |
672 | } | |
54847287 EM |
673 | //now format custom data. |
674 | foreach ($params as $key => $field) { | |
639e4f37 EM |
675 | if (in_array($key, $metadataBlocks, TRUE)) { |
676 | // This location block is already fully handled at this point. | |
677 | continue; | |
678 | } | |
54847287 EM |
679 | if (is_array($field)) { |
680 | $isAddressCustomField = FALSE; | |
639e4f37 | 681 | |
54847287 EM |
682 | foreach ($field as $value) { |
683 | $break = FALSE; | |
684 | if (is_array($value)) { | |
685 | foreach ($value as $name => $testForEmpty) { | |
686 | if ($addressCustomFieldID = CRM_Core_BAO_CustomField::getKeyID($name)) { | |
687 | $isAddressCustomField = TRUE; | |
688 | break; | |
689 | } | |
639e4f37 EM |
690 | |
691 | if (($testForEmpty === '' || $testForEmpty == NULL)) { | |
54847287 EM |
692 | $break = TRUE; |
693 | break; | |
694 | } | |
695 | } | |
696 | } | |
697 | else { | |
698 | $break = TRUE; | |
699 | } | |
700 | ||
701 | if (!$break) { | |
702 | if (!empty($value['location_type_id'])) { | |
703 | $this->formatLocationBlock($value, $formatted); | |
704 | } | |
54847287 EM |
705 | } |
706 | } | |
707 | if (!$isAddressCustomField) { | |
708 | continue; | |
709 | } | |
710 | } | |
711 | ||
712 | $formatValues = [ | |
713 | $key => $field, | |
714 | ]; | |
715 | ||
54847287 EM |
716 | if ($key == 'id' && isset($field)) { |
717 | $formatted[$key] = $field; | |
718 | } | |
719 | $this->formatContactParameters($formatValues, $formatted); | |
720 | ||
721 | //Handling Custom Data | |
722 | // note: Address custom fields will be handled separately inside formatContactParameters | |
723 | if (($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && | |
724 | array_key_exists($customFieldID, $customFields) && | |
725 | !array_key_exists($customFieldID, $addressCustomFields) | |
726 | ) { | |
727 | ||
728 | $extends = $customFields[$customFieldID]['extends'] ?? NULL; | |
729 | $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL; | |
730 | $dataType = $customFields[$customFieldID]['data_type'] ?? NULL; | |
731 | $serialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]); | |
732 | ||
733 | if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) { | |
734 | $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE); | |
735 | foreach ($customOption as $customValue) { | |
736 | $val = $customValue['value'] ?? NULL; | |
737 | $label = strtolower($customValue['label'] ?? ''); | |
738 | $value = strtolower(trim($formatted[$key])); | |
739 | if (($value == $label) || ($value == strtolower($val))) { | |
740 | $params[$key] = $formatted[$key] = $val; | |
741 | } | |
742 | } | |
743 | } | |
744 | elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) { | |
745 | $mulValues = explode(',', $formatted[$key]); | |
746 | $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE); | |
747 | $formatted[$key] = []; | |
748 | $params[$key] = []; | |
749 | foreach ($mulValues as $v1) { | |
750 | foreach ($customOption as $v2) { | |
751 | if ((strtolower($v2['label']) == strtolower(trim($v1))) || | |
752 | (strtolower($v2['value']) == strtolower(trim($v1))) | |
753 | ) { | |
754 | if ($htmlType == 'CheckBox') { | |
755 | $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1; | |
756 | } | |
757 | else { | |
758 | $params[$key][] = $formatted[$key][] = $v2['value']; | |
759 | } | |
760 | } | |
761 | } | |
762 | } | |
763 | } | |
764 | } | |
765 | } | |
766 | ||
767 | if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && array_key_exists($customFieldID, $customFields) && | |
768 | !array_key_exists($customFieldID, $addressCustomFields) | |
769 | ) { | |
770 | // @todo calling api functions directly is not supported | |
771 | _civicrm_api3_custom_format_params($params, $formatted, $extends); | |
772 | } | |
773 | ||
774 | // to check if not update mode and unset the fields with empty value. | |
775 | if (!$this->_updateWithId && array_key_exists('custom', $formatted)) { | |
776 | foreach ($formatted['custom'] as $customKey => $customvalue) { | |
777 | if (empty($formatted['custom'][$customKey][-1]['is_required'])) { | |
778 | $formatted['custom'][$customKey][-1]['is_required'] = $customFields[$customKey]['is_required']; | |
779 | } | |
780 | $emptyValue = $customvalue[-1]['value'] ?? NULL; | |
781 | if (!isset($emptyValue)) { | |
782 | unset($formatted['custom'][$customKey]); | |
783 | } | |
784 | } | |
785 | } | |
786 | ||
787 | // parse street address, CRM-5450 | |
788 | if ($this->_parseStreetAddress) { | |
789 | if (array_key_exists('address', $formatted) && is_array($formatted['address'])) { | |
790 | foreach ($formatted['address'] as $instance => & $address) { | |
791 | $streetAddress = $address['street_address'] ?? NULL; | |
792 | if (empty($streetAddress)) { | |
793 | continue; | |
794 | } | |
795 | // parse address field. | |
796 | $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($streetAddress); | |
797 | ||
798 | //street address consider to be parsed properly, | |
799 | //If we get street_name and street_number. | |
800 | if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) { | |
801 | $parsedFields = array_fill_keys(array_keys($parsedFields), ''); | |
802 | } | |
803 | ||
804 | // merge parse address w/ main address block. | |
805 | $address = array_merge($address, $parsedFields); | |
806 | } | |
807 | } | |
808 | } | |
809 | } | |
810 | ||
6a488035 | 811 | /** |
ceb10dc7 | 812 | * Get the array of successfully imported contact id's |
6a488035 TO |
813 | * |
814 | * @return array | |
6a488035 | 815 | */ |
a4acf773 | 816 | public function getImportedContacts() { |
6a488035 TO |
817 | return $this->_newContacts; |
818 | } | |
819 | ||
820 | /** | |
ceb10dc7 | 821 | * Get the array of successfully imported related contact id's |
6a488035 TO |
822 | * |
823 | * @return array | |
6a488035 | 824 | */ |
00be9182 | 825 | public function &getRelatedImportedContacts() { |
6a488035 TO |
826 | return $this->_newRelatedContacts; |
827 | } | |
828 | ||
6a488035 | 829 | /** |
fe482240 | 830 | * Check if an error in custom data. |
6a488035 | 831 | * |
c490a46a | 832 | * @param array $params |
77c5b619 TO |
833 | * @param string $errorMessage |
834 | * A string containing all the error-fields. | |
fd31fa4c EM |
835 | * |
836 | * @param null $csType | |
6a488035 | 837 | */ |
12a8af61 | 838 | public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL) { |
412585fb | 839 | $dateType = CRM_Core_Session::singleton()->get("dateTypes"); |
9547a2f9 | 840 | $errors = []; |
6a488035 | 841 | |
a7488080 | 842 | if (!empty($params['contact_sub_type'])) { |
9c1bc317 | 843 | $csType = $params['contact_sub_type'] ?? NULL; |
6a488035 | 844 | } |
d4c8a770 | 845 | |
a7488080 | 846 | if (empty($params['contact_type'])) { |
6a488035 TO |
847 | $params['contact_type'] = 'Individual'; |
848 | } | |
6aa6310f SL |
849 | |
850 | // get array of subtypes - CRM-18708 | |
2d35bc78 | 851 | if (in_array($csType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) { |
6aa6310f SL |
852 | $csType = self::getSubtypes($params['contact_type']); |
853 | } | |
854 | ||
855 | if (is_array($csType)) { | |
856 | // fetch custom fields for every subtype and add it to $customFields array | |
857 | // CRM-18708 | |
be2fb01f | 858 | $customFields = []; |
6aa6310f SL |
859 | foreach ($csType as $cType) { |
860 | $customFields += CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $cType); | |
861 | } | |
862 | } | |
863 | else { | |
864 | $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType); | |
865 | } | |
6a488035 TO |
866 | |
867 | $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address'); | |
0636f0c8 | 868 | $parser = new CRM_Contact_Import_Parser_Contact(); |
6a488035 TO |
869 | foreach ($params as $key => $value) { |
870 | if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) { | |
6a488035 TO |
871 | //For address custom fields, we do get actual custom field value as an inner array of |
872 | //values so need to modify | |
018c9e26 EM |
873 | if (!array_key_exists($customFieldID, $customFields)) { |
874 | return ts('field ID'); | |
6a488035 | 875 | } |
018c9e26 EM |
876 | /* check if it's a valid custom field id */ |
877 | $errors[] = $parser->validateCustomField($customFieldID, $value, $customFields[$customFieldID], $dateType); | |
6a488035 | 878 | } |
12a8af61 | 879 | elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) { |
6a488035 TO |
880 | //CRM-5125 |
881 | //supporting custom data of related contact subtypes | |
12a8af61 | 882 | $relation = $key; |
6a488035 | 883 | if (!empty($relation)) { |
a8ea3922 | 884 | [$id, $first, $second] = CRM_Utils_System::explode('_', $relation, 3); |
6a488035 TO |
885 | $direction = "contact_sub_type_$second"; |
886 | $relationshipType = new CRM_Contact_BAO_RelationshipType(); | |
887 | $relationshipType->id = $id; | |
888 | if ($relationshipType->find(TRUE)) { | |
889 | if (isset($relationshipType->$direction)) { | |
890 | $params[$key]['contact_sub_type'] = $relationshipType->$direction; | |
891 | } | |
892 | } | |
6a488035 TO |
893 | } |
894 | ||
12a8af61 | 895 | self::isErrorInCustomData($params[$key], $errorMessage, $csType); |
6a488035 TO |
896 | } |
897 | } | |
9547a2f9 | 898 | if ($errors) { |
208177a5 | 899 | $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', array_filter($errors)); |
9547a2f9 | 900 | } |
6a488035 TO |
901 | } |
902 | ||
6a488035 | 903 | /** |
100fef9d | 904 | * Check if an error in Core( non-custom fields ) field |
6a488035 | 905 | * |
c490a46a | 906 | * @param array $params |
77c5b619 TO |
907 | * @param string $errorMessage |
908 | * A string containing all the error-fields. | |
6a488035 | 909 | */ |
00be9182 | 910 | public function isErrorInCoreData($params, &$errorMessage) { |
9547a2f9 | 911 | $errors = []; |
e0b8f9a9 EM |
912 | if (!empty($params['contact_sub_type']) && !CRM_Contact_BAO_ContactType::isExtendsContactType($params['contact_sub_type'], $params['contact_type'])) { |
913 | $errors[] = ts('Mismatched or Invalid Contact Subtype.'); | |
914 | } | |
915 | ||
6a488035 TO |
916 | foreach ($params as $key => $value) { |
917 | if ($value) { | |
6a488035 TO |
918 | |
919 | switch ($key) { | |
6a488035 TO |
920 | case 'do_not_email': |
921 | case 'do_not_phone': | |
922 | case 'do_not_mail': | |
923 | case 'do_not_sms': | |
924 | case 'do_not_trade': | |
925 | if (CRM_Utils_Rule::boolean($value) == FALSE) { | |
926 | $key = ucwords(str_replace("_", " ", $key)); | |
9547a2f9 | 927 | $errors[] = $key; |
6a488035 TO |
928 | } |
929 | break; | |
930 | ||
6a488035 TO |
931 | default: |
932 | if (is_array($params[$key]) && isset($params[$key]["contact_type"])) { | |
933 | //check for any relationship data ,FIX ME | |
934 | self::isErrorInCoreData($params[$key], $errorMessage); | |
935 | } | |
936 | } | |
937 | } | |
938 | } | |
9547a2f9 EM |
939 | if ($errors) { |
940 | $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', $errors); | |
941 | } | |
6a488035 TO |
942 | } |
943 | ||
944 | /** | |
fe482240 | 945 | * Ckeck a value present or not in a array. |
6a488035 | 946 | * |
77b97be7 EM |
947 | * @param $value |
948 | * @param $valueArray | |
949 | * | |
c14d6dd1 | 950 | * @return bool |
6a488035 | 951 | */ |
9cb19e4a | 952 | public static function in_value($value, $valueArray) { |
6a488035 TO |
953 | foreach ($valueArray as $key => $v) { |
954 | //fix for CRM-1514 | |
955 | if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) { | |
956 | return TRUE; | |
957 | } | |
958 | } | |
959 | return FALSE; | |
960 | } | |
961 | ||
962 | /** | |
100fef9d | 963 | * Build error-message containing error-fields |
6a488035 | 964 | * |
341c643b | 965 | * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function. |
966 | * | |
967 | * @todo just say no! | |
968 | * | |
77c5b619 TO |
969 | * @param string $errorName |
970 | * A string containing error-field name. | |
971 | * @param string $errorMessage | |
972 | * A string containing all the error-fields, where the new errorName is concatenated. | |
6a488035 | 973 | * |
6a488035 | 974 | */ |
00be9182 | 975 | public static function addToErrorMsg($errorName, &$errorMessage) { |
6a488035 TO |
976 | if ($errorMessage) { |
977 | $errorMessage .= "; $errorName"; | |
978 | } | |
979 | else { | |
980 | $errorMessage = $errorName; | |
981 | } | |
982 | } | |
983 | ||
984 | /** | |
fe482240 | 985 | * Method for creating contact. |
54957108 | 986 | * |
987 | * @param array $formatted | |
988 | * @param array $contactFields | |
989 | * @param int $onDuplicate | |
990 | * @param int $contactId | |
991 | * @param bool $requiredCheck | |
992 | * @param int $dedupeRuleGroupID | |
993 | * | |
eaedbd91 EM |
994 | * @return array|\CRM_Contact_BAO_Contact |
995 | * If a duplicate is found an array is returned, otherwise CRM_Contact_BAO_Contact | |
6a488035 | 996 | */ |
00be9182 | 997 | public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) { |
6a488035 | 998 | $dupeCheck = FALSE; |
6a488035 TO |
999 | $newContact = NULL; |
1000 | ||
a05662ef | 1001 | if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser::DUPLICATE_NOCHECK)) { |
51ccfbbe | 1002 | $dupeCheck = (bool) ($onDuplicate); |
6a488035 TO |
1003 | } |
1004 | ||
e77978b7 EM |
1005 | if ($dupeCheck) { |
1006 | // @todo this is already done in lookupContactID | |
1007 | // the differences are that a couple of functions are callled in between | |
1008 | // and that call doesn't error out if multiple are found. - once | |
1009 | // those 2 things are fixed this can go entirely. | |
1010 | $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($formatted, $formatted['contact_type'], 'Unsupervised', [], FALSE, $dedupeRuleGroupID); | |
1011 | ||
1012 | if ($ids != NULL) { | |
1013 | return [ | |
1014 | 'is_error' => 1, | |
1015 | 'error_message' => [ | |
1016 | 'code' => CRM_Core_Error::DUPLICATE_CONTACT, | |
1017 | 'params' => $ids, | |
1018 | 'level' => 'Fatal', | |
1019 | 'message' => 'Found matching contacts: ' . implode(',', $ids), | |
1020 | ], | |
1021 | ]; | |
eba173d6 | 1022 | } |
6a488035 | 1023 | } |
6a488035 | 1024 | |
9789bc5f | 1025 | if ($contactId) { |
1026 | $this->formatParams($formatted, $onDuplicate, (int) $contactId); | |
1027 | } | |
6a488035 | 1028 | |
9789bc5f | 1029 | // Resetting and rebuilding cache could be expensive. |
1030 | CRM_Core_Config::setPermitCacheFlushMode(FALSE); | |
1031 | ||
1032 | // If a user has logged in, or accessed via a checksum | |
1033 | // Then deliberately 'blanking' a value in the profile should remove it from their record | |
1034 | // @todo this should either be TRUE or FALSE in the context of import - once | |
1035 | // we figure out which we can remove all the rest. | |
1036 | // Also note the meaning of this parameter is less than it used to | |
1037 | // be following block cleanup. | |
1038 | $formatted['updateBlankLocInfo'] = TRUE; | |
1039 | if ((CRM_Core_Session::singleton()->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0) { | |
1040 | $formatted['updateBlankLocInfo'] = FALSE; | |
1041 | } | |
29f811de | 1042 | |
897732d6 | 1043 | [$data, $contactDetails] = $this->formatProfileContactParams($formatted, $contactFields, $contactId, $formatted['contact_type']); |
9789bc5f | 1044 | |
1045 | // manage is_opt_out | |
1046 | if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) { | |
1047 | $wasOptOut = $contactDetails['is_opt_out'] ?? FALSE; | |
1048 | $isOptOut = $formatted['is_opt_out']; | |
1049 | $data['is_opt_out'] = $isOptOut; | |
1050 | // on change, create new civicrm_subscription_history entry | |
1051 | if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) { | |
1052 | $shParams = [ | |
1053 | 'contact_id' => $contactDetails['contact_id'], | |
1054 | 'status' => $isOptOut ? 'Removed' : 'Added', | |
1055 | 'method' => 'Web', | |
1056 | ]; | |
1057 | CRM_Contact_BAO_SubscriptionHistory::create($shParams); | |
29f811de | 1058 | } |
9789bc5f | 1059 | } |
29f811de | 1060 | |
cdfa6649 | 1061 | $contact = civicrm_api3('Contact', 'create', $data); |
1062 | $cid = $contact['id']; | |
29f811de | 1063 | |
9789bc5f | 1064 | CRM_Core_Config::setPermitCacheFlushMode(TRUE); |
6a488035 | 1065 | |
9789bc5f | 1066 | $contact = [ |
1067 | 'contact_id' => $cid, | |
1068 | ]; | |
6a488035 | 1069 | |
9789bc5f | 1070 | $defaults = []; |
1071 | $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults); | |
6a488035 TO |
1072 | |
1073 | //get the id of the contact whose street address is not parsable, CRM-5886 | |
d8a87436 | 1074 | if ($this->_parseStreetAddress && is_object($newContact) && property_exists($newContact, 'address') && $newContact->address) { |
6a488035 | 1075 | foreach ($newContact->address as $address) { |
8cc574cf | 1076 | if (!empty($address['street_address']) && (empty($address['street_number']) || empty($address['street_name']))) { |
be2fb01f | 1077 | $this->_unparsedStreetAddressContacts[] = [ |
6a488035 TO |
1078 | 'id' => $newContact->id, |
1079 | 'streetAddress' => $address['street_address'], | |
be2fb01f | 1080 | ]; |
6a488035 TO |
1081 | } |
1082 | } | |
1083 | } | |
1084 | return $newContact; | |
1085 | } | |
1086 | ||
b248c095 EM |
1087 | /** |
1088 | * Legacy format profile contact parameters. | |
1089 | * | |
1090 | * This is a formerly shared function - most of the stuff in it probably does | |
1091 | * nothing but copied here to star unravelling that... | |
1092 | * | |
1093 | * @param array $params | |
1094 | * @param array $fields | |
1095 | * @param int|null $contactID | |
b248c095 | 1096 | * @param string|null $ctype |
b248c095 EM |
1097 | * |
1098 | * @return array | |
1099 | */ | |
1100 | private function formatProfileContactParams( | |
1101 | &$params, | |
1102 | $fields, | |
1103 | $contactID = NULL, | |
897732d6 | 1104 | $ctype = NULL |
b248c095 EM |
1105 | ) { |
1106 | ||
1107 | $data = $contactDetails = []; | |
1108 | ||
1109 | // get the contact details (hier) | |
1110 | if ($contactID) { | |
1111 | $details = CRM_Contact_BAO_Contact::getHierContactDetails($contactID, $fields); | |
1112 | ||
1113 | $contactDetails = $details[$contactID]; | |
1114 | $data['contact_type'] = $contactDetails['contact_type'] ?? NULL; | |
1115 | $data['contact_sub_type'] = $contactDetails['contact_sub_type'] ?? NULL; | |
1116 | } | |
1117 | else { | |
1118 | //we should get contact type only if contact | |
897732d6 | 1119 | if ($ctype) { |
b248c095 EM |
1120 | $data['contact_type'] = $ctype; |
1121 | } | |
1122 | else { | |
1123 | $data['contact_type'] = 'Individual'; | |
1124 | } | |
1125 | } | |
1126 | ||
1127 | //fix contact sub type CRM-5125 | |
1128 | if (array_key_exists('contact_sub_type', $params) && | |
1129 | !empty($params['contact_sub_type']) | |
1130 | ) { | |
1131 | $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type']); | |
1132 | } | |
1133 | elseif (array_key_exists('contact_sub_type_hidden', $params) && | |
1134 | !empty($params['contact_sub_type_hidden']) | |
1135 | ) { | |
1136 | // if profile was used, and had any subtype, we obtain it from there | |
1137 | //CRM-13596 - add to existing contact types, rather than overwriting | |
1138 | if (empty($data['contact_sub_type'])) { | |
1139 | // If we don't have a contact ID the $data['contact_sub_type'] will not be defined... | |
1140 | $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']); | |
1141 | } | |
1142 | else { | |
1143 | $data_contact_sub_type_arr = CRM_Utils_Array::explodePadded($data['contact_sub_type']); | |
1144 | if (!in_array($params['contact_sub_type_hidden'], $data_contact_sub_type_arr)) { | |
1145 | //CRM-20517 - make sure contact_sub_type gets the correct delimiters | |
1146 | $data['contact_sub_type'] = trim($data['contact_sub_type'], CRM_Core_DAO::VALUE_SEPARATOR); | |
1147 | $data['contact_sub_type'] = CRM_Core_DAO::VALUE_SEPARATOR . $data['contact_sub_type'] . CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']); | |
1148 | } | |
1149 | } | |
1150 | } | |
1151 | ||
1152 | if ($ctype == 'Organization') { | |
1153 | $data['organization_name'] = $contactDetails['organization_name'] ?? NULL; | |
1154 | } | |
1155 | elseif ($ctype == 'Household') { | |
1156 | $data['household_name'] = $contactDetails['household_name'] ?? NULL; | |
1157 | } | |
1158 | ||
1159 | $locationType = []; | |
1160 | $count = 1; | |
1161 | ||
1162 | if ($contactID) { | |
1163 | //add contact id | |
1164 | $data['contact_id'] = $contactID; | |
1165 | $primaryLocationType = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID); | |
1166 | } | |
1167 | else { | |
1168 | $defaultLocation = CRM_Core_BAO_LocationType::getDefault(); | |
1169 | $defaultLocationId = $defaultLocation->id; | |
1170 | } | |
1171 | ||
1172 | $billingLocationTypeId = CRM_Core_BAO_LocationType::getBilling(); | |
1173 | ||
b248c095 | 1174 | $multiplFields = ['url']; |
b248c095 | 1175 | |
b248c095 EM |
1176 | $session = CRM_Core_Session::singleton(); |
1177 | foreach ($params as $key => $value) { | |
1178 | [$fieldName, $locTypeId, $typeId] = CRM_Utils_System::explode('-', $key, 3); | |
1179 | ||
1180 | if ($locTypeId == 'Primary') { | |
1181 | if ($contactID) { | |
639e4f37 | 1182 | $locTypeId = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID, FALSE, 'address'); |
b248c095 EM |
1183 | $primaryLocationType = $locTypeId; |
1184 | } | |
1185 | else { | |
1186 | $locTypeId = $defaultLocationId; | |
1187 | } | |
1188 | } | |
1189 | ||
1190 | if (is_numeric($locTypeId) && | |
1191 | !in_array($fieldName, $multiplFields) && | |
1192 | substr($fieldName, 0, 7) != 'custom_' | |
1193 | ) { | |
1194 | $index = $locTypeId; | |
1195 | ||
1196 | if (is_numeric($typeId)) { | |
1197 | $index .= '-' . $typeId; | |
1198 | } | |
1199 | if (!in_array($index, $locationType)) { | |
1200 | $locationType[$count] = $index; | |
1201 | $count++; | |
1202 | } | |
1203 | ||
1204 | $loc = CRM_Utils_Array::key($index, $locationType); | |
1205 | ||
4586ae70 | 1206 | $blockName = strtolower($this->getFieldEntity($fieldName)); |
b248c095 EM |
1207 | |
1208 | $data[$blockName][$loc]['location_type_id'] = $locTypeId; | |
1209 | ||
1210 | //set is_billing true, for location type "Billing" | |
1211 | if ($locTypeId == $billingLocationTypeId) { | |
1212 | $data[$blockName][$loc]['is_billing'] = 1; | |
1213 | } | |
1214 | ||
1215 | if ($contactID) { | |
1216 | //get the primary location type | |
1217 | if ($locTypeId == $primaryLocationType) { | |
1218 | $data[$blockName][$loc]['is_primary'] = 1; | |
1219 | } | |
1220 | } | |
1221 | elseif ($locTypeId == $defaultLocationId) { | |
1222 | $data[$blockName][$loc]['is_primary'] = 1; | |
1223 | } | |
1224 | ||
639e4f37 | 1225 | if (0) { |
b248c095 EM |
1226 | } |
1227 | else { | |
1228 | if ($fieldName === 'state_province') { | |
1229 | // CRM-3393 | |
1230 | if (is_numeric($value) && ((int ) $value) >= 1000) { | |
1231 | $data['address'][$loc]['state_province_id'] = $value; | |
1232 | } | |
1233 | elseif (empty($value)) { | |
1234 | $data['address'][$loc]['state_province_id'] = ''; | |
1235 | } | |
1236 | else { | |
1237 | $data['address'][$loc]['state_province'] = $value; | |
1238 | } | |
1239 | } | |
24948d41 EM |
1240 | elseif ($fieldName === 'country_id') { |
1241 | $data['address'][$loc]['country_id'] = $value; | |
b248c095 EM |
1242 | } |
1243 | elseif ($fieldName === 'county') { | |
1244 | $data['address'][$loc]['county_id'] = $value; | |
1245 | } | |
1246 | elseif ($fieldName == 'address_name') { | |
1247 | $data['address'][$loc]['name'] = $value; | |
1248 | } | |
1249 | elseif (substr($fieldName, 0, 14) === 'address_custom') { | |
1250 | $data['address'][$loc][substr($fieldName, 8)] = $value; | |
1251 | } | |
1252 | else { | |
1253 | $data[$blockName][$loc][$fieldName] = $value; | |
1254 | } | |
1255 | } | |
1256 | } | |
1257 | else { | |
80e9f1a2 | 1258 | if (($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key))) { |
b248c095 EM |
1259 | // for autocomplete transfer hidden value instead of label |
1260 | if ($params[$key] && isset($params[$key . '_id'])) { | |
1261 | $value = $params[$key . '_id']; | |
1262 | } | |
1263 | ||
1264 | // we need to append time with date | |
1265 | if ($params[$key] && isset($params[$key . '_time'])) { | |
1266 | $value .= ' ' . $params[$key . '_time']; | |
1267 | } | |
1268 | ||
1269 | // if auth source is not checksum / login && $value is blank, do not proceed - CRM-10128 | |
1270 | if (($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 && | |
1271 | ($value == '' || !isset($value)) | |
1272 | ) { | |
1273 | continue; | |
1274 | } | |
1275 | ||
1276 | $valueId = NULL; | |
b248c095 EM |
1277 | |
1278 | //CRM-13596 - check for contact_sub_type_hidden first | |
1279 | if (array_key_exists('contact_sub_type_hidden', $params)) { | |
1280 | $type = $params['contact_sub_type_hidden']; | |
1281 | } | |
1282 | else { | |
1283 | $type = $data['contact_type']; | |
1284 | if (!empty($data['contact_sub_type'])) { | |
1285 | $type = CRM_Utils_Array::explodePadded($data['contact_sub_type']); | |
1286 | } | |
1287 | } | |
1288 | ||
1289 | CRM_Core_BAO_CustomField::formatCustomField($customFieldId, | |
1290 | $data['custom'], | |
1291 | $value, | |
1292 | $type, | |
1293 | $valueId, | |
1294 | $contactID, | |
1295 | FALSE, | |
1296 | FALSE | |
1297 | ); | |
1298 | } | |
1299 | elseif ($key === 'edit') { | |
1300 | continue; | |
1301 | } | |
1302 | else { | |
1303 | if ($key === 'location') { | |
1304 | foreach ($value as $locationTypeId => $field) { | |
1305 | foreach ($field as $block => $val) { | |
1306 | if ($block === 'address' && array_key_exists('address_name', $val)) { | |
1307 | $value[$locationTypeId][$block]['name'] = $value[$locationTypeId][$block]['address_name']; | |
1308 | } | |
1309 | } | |
1310 | } | |
1311 | } | |
639e4f37 | 1312 | if (in_array($key, ['nick_name', 'job_title', 'middle_name', 'birth_date', 'gender_id', 'current_employer', 'prefix_id', 'suffix_id']) |
b248c095 EM |
1313 | && ($value == '' || !isset($value)) && |
1314 | ($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 || | |
1315 | ($key === 'current_employer' && empty($params['current_employer']))) { | |
1316 | // CRM-10128: if auth source is not checksum / login && $value is blank, do not fill $data with empty value | |
1317 | // to avoid update with empty values | |
1318 | continue; | |
1319 | } | |
1320 | else { | |
1321 | $data[$key] = $value; | |
1322 | } | |
1323 | } | |
1324 | } | |
1325 | } | |
1326 | ||
1327 | if (!isset($data['contact_type'])) { | |
1328 | $data['contact_type'] = 'Individual'; | |
1329 | } | |
1330 | ||
1331 | //set the values for checkboxes (do_not_email, do_not_mail, do_not_trade, do_not_phone) | |
1332 | $privacy = CRM_Core_SelectValues::privacy(); | |
1333 | foreach ($privacy as $key => $value) { | |
1334 | if (array_key_exists($key, $fields)) { | |
1335 | // do not reset values for existing contacts, if fields are added to a profile | |
1336 | if (array_key_exists($key, $params)) { | |
1337 | $data[$key] = $params[$key]; | |
1338 | if (empty($params[$key])) { | |
1339 | $data[$key] = 0; | |
1340 | } | |
1341 | } | |
1342 | elseif (!$contactID) { | |
1343 | $data[$key] = 0; | |
1344 | } | |
1345 | } | |
1346 | } | |
1347 | ||
1348 | return [$data, $contactDetails]; | |
1349 | } | |
1350 | ||
6a488035 | 1351 | /** |
fe482240 | 1352 | * Format params for update and fill mode. |
6a488035 | 1353 | * |
5a4f6742 CW |
1354 | * @param array $params |
1355 | * reference to an array containing all the. | |
16b10e64 | 1356 | * values for import |
5a4f6742 CW |
1357 | * @param int $onDuplicate |
1358 | * @param int $cid | |
1359 | * contact id. | |
6a488035 | 1360 | */ |
00be9182 | 1361 | public function formatParams(&$params, $onDuplicate, $cid) { |
a05662ef | 1362 | if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) { |
6a488035 TO |
1363 | return; |
1364 | } | |
1365 | ||
be2fb01f | 1366 | $contactParams = [ |
6a488035 | 1367 | 'contact_id' => $cid, |
be2fb01f | 1368 | ]; |
6a488035 | 1369 | |
be2fb01f | 1370 | $defaults = []; |
6a488035 TO |
1371 | $contactObj = CRM_Contact_BAO_Contact::retrieve($contactParams, $defaults); |
1372 | ||
8a6b2185 | 1373 | $modeFill = ($onDuplicate == CRM_Import_Parser::DUPLICATE_FILL); |
6a488035 | 1374 | |
0b330e6d | 1375 | $groupTree = CRM_Core_BAO_CustomGroup::getTree($params['contact_type'], NULL, $cid, 0, NULL); |
6a488035 TO |
1376 | CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $defaults, FALSE, FALSE); |
1377 | ||
be2fb01f | 1378 | $locationFields = [ |
6a488035 | 1379 | 'address' => 'address', |
be2fb01f | 1380 | ]; |
6a488035 TO |
1381 | |
1382 | $contact = get_object_vars($contactObj); | |
1383 | ||
1384 | foreach ($params as $key => $value) { | |
1385 | if ($key == 'id' || $key == 'contact_type') { | |
1386 | continue; | |
1387 | } | |
1388 | ||
1389 | if (array_key_exists($key, $locationFields)) { | |
1390 | continue; | |
1391 | } | |
f3f321c7 | 1392 | |
60110bfa EM |
1393 | if ($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key)) { |
1394 | $custom_params = ['id' => $contact['id'], 'return' => $key]; | |
1395 | $getValue = civicrm_api3('Contact', 'getvalue', $custom_params); | |
1396 | if (empty($getValue)) { | |
1397 | unset($getValue); | |
6a488035 | 1398 | } |
60110bfa EM |
1399 | } |
1400 | else { | |
1401 | $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $key); | |
1402 | } | |
1403 | if ($key == 'contact_source') { | |
1404 | $params['source'] = $params[$key]; | |
1405 | unset($params[$key]); | |
1406 | } | |
6a488035 | 1407 | |
60110bfa EM |
1408 | if ($modeFill && isset($getValue)) { |
1409 | unset($params[$key]); | |
1410 | if ($customFieldId) { | |
1411 | // Extra values must be unset to ensure the values are not | |
1412 | // imported. | |
1413 | unset($params['custom'][$customFieldId]); | |
6a488035 TO |
1414 | } |
1415 | } | |
1416 | } | |
1417 | ||
1418 | foreach ($locationFields as $locKeys) { | |
e01bf597 | 1419 | if (isset($params[$locKeys]) && is_array($params[$locKeys])) { |
6a488035 TO |
1420 | foreach ($params[$locKeys] as $key => $value) { |
1421 | if ($modeFill) { | |
1422 | $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $locKeys); | |
1423 | ||
1424 | if (isset($getValue)) { | |
1425 | foreach ($getValue as $cnt => $values) { | |
639e4f37 EM |
1426 | if ((!empty($getValue[$cnt]['location_type_id']) && !empty($params[$locKeys][$key]['location_type_id'])) && $getValue[$cnt]['location_type_id'] == $params[$locKeys][$key]['location_type_id']) { |
1427 | unset($params[$locKeys][$key]); | |
6a488035 TO |
1428 | } |
1429 | } | |
1430 | } | |
1431 | } | |
1432 | } | |
1433 | if (count($params[$locKeys]) == 0) { | |
1434 | unset($params[$locKeys]); | |
1435 | } | |
1436 | } | |
1437 | } | |
1438 | } | |
1439 | ||
1440 | /** | |
100fef9d | 1441 | * Convert any given date string to default date array. |
6a488035 | 1442 | * |
77c5b619 TO |
1443 | * @param array $params |
1444 | * Has given date-format. | |
1445 | * @param array $formatted | |
1446 | * Store formatted date in this array. | |
1447 | * @param int $dateType | |
1448 | * Type of date. | |
1449 | * @param string $dateParam | |
1450 | * Index of params. | |
6a488035 | 1451 | */ |
6ae6166d | 1452 | public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) { |
6a488035 TO |
1453 | //fix for CRM-2687 |
1454 | CRM_Utils_Date::convertToDefaultDate($params, $dateType, $dateParam); | |
1455 | $formatted[$dateParam] = CRM_Utils_Date::processDate($params[$dateParam]); | |
1456 | } | |
1457 | ||
6a488035 | 1458 | /** |
100fef9d | 1459 | * Generate status and error message for unparsed street address records. |
6a488035 | 1460 | * |
77c5b619 TO |
1461 | * @param array $values |
1462 | * The array of values belonging to each row. | |
77b97be7 EM |
1463 | * @param $returnCode |
1464 | * | |
1465 | * @return int | |
6a488035 | 1466 | */ |
57d6aced | 1467 | private function processMessage(&$values, $returnCode) { |
6a488035 | 1468 | if (empty($this->_unparsedStreetAddressContacts)) { |
12d121eb | 1469 | $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', ''); |
6a488035 TO |
1470 | } |
1471 | else { | |
1472 | $errorMessage = ts("Record imported successfully but unable to parse the street address: "); | |
1473 | foreach ($this->_unparsedStreetAddressContacts as $contactInfo => $contactValue) { | |
1474 | $contactUrl = CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE); | |
1475 | $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>"; | |
1476 | } | |
1477 | array_unshift($values, $errorMessage); | |
a05662ef | 1478 | $returnCode = CRM_Import_Parser::UNPARSED_ADDRESS_WARNING; |
12d121eb | 1479 | $this->setImportStatus((int) ($values[count($values) - 1]), 'ERROR', $errorMessage); |
6a488035 | 1480 | } |
6a488035 TO |
1481 | return $returnCode; |
1482 | } | |
1483 | ||
6aa6310f SL |
1484 | /** |
1485 | * get subtypes given the contact type | |
1486 | * | |
1487 | * @param string $contactType | |
1488 | * @return array $subTypes | |
1489 | */ | |
1490 | public static function getSubtypes($contactType) { | |
be2fb01f | 1491 | $subTypes = []; |
6aa6310f SL |
1492 | $types = CRM_Contact_BAO_ContactType::subTypeInfo($contactType); |
1493 | ||
1494 | if (count($types) > 0) { | |
1495 | foreach ($types as $type) { | |
1496 | $subTypes[] = $type['name']; | |
1497 | } | |
1498 | } | |
1499 | return $subTypes; | |
1500 | } | |
1501 | ||
65070890 | 1502 | /** |
1503 | * Get the possible contact matches. | |
1504 | * | |
1505 | * 1) the chosen dedupe rule falling back to | |
1506 | * 2) a check for the external ID. | |
1507 | * | |
0e480632 | 1508 | * @see https://issues.civicrm.org/jira/browse/CRM-17275 |
65070890 | 1509 | * |
1510 | * @param array $params | |
64623d6c EM |
1511 | * @param int|null $extIDMatch |
1512 | * @param int|null $dedupeRuleID | |
65070890 | 1513 | * |
64623d6c EM |
1514 | * @return int|null |
1515 | * IDs of a possible. | |
65070890 | 1516 | * |
1517 | * @throws \CRM_Core_Exception | |
1518 | * @throws \CiviCRM_API3_Exception | |
1519 | */ | |
64623d6c EM |
1520 | protected function getPossibleContactMatch(array $params, ?int $extIDMatch, ?int $dedupeRuleID): ?int { |
1521 | $checkParams = ['check_permissions' => FALSE, 'match' => $params, 'dedupe_rule_id' => $dedupeRuleID]; | |
65070890 | 1522 | $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams); |
1523 | if (!$extIDMatch) { | |
64623d6c EM |
1524 | // Historically we have used the last ID - it is not clear if this was |
1525 | // deliberate. | |
1526 | return array_key_last($possibleMatches['values']); | |
65070890 | 1527 | } |
1528 | if ($possibleMatches['count']) { | |
c3f7ab62 | 1529 | if (array_key_exists($extIDMatch, $possibleMatches['values'])) { |
64623d6c | 1530 | return $extIDMatch; |
65070890 | 1531 | } |
2d8361a4 | 1532 | throw new CRM_Core_Exception(ts( |
1533 | 'Matching this contact based on the de-dupe rule would cause an external ID conflict')); | |
65070890 | 1534 | } |
64623d6c | 1535 | return $extIDMatch; |
65070890 | 1536 | } |
1537 | ||
1881b7b0 | 1538 | /** |
1539 | * Format the form mapping parameters ready for the parser. | |
1540 | * | |
1541 | * @param int $count | |
1542 | * Number of rows. | |
1543 | * | |
1544 | * @return array $parserParameters | |
1545 | */ | |
1546 | public static function getParameterForParser($count) { | |
be2fb01f | 1547 | $baseArray = []; |
1881b7b0 | 1548 | for ($i = 0; $i < $count; $i++) { |
1549 | $baseArray[$i] = NULL; | |
1550 | } | |
1551 | $parserParameters['mapperLocType'] = $baseArray; | |
1552 | $parserParameters['mapperPhoneType'] = $baseArray; | |
1553 | $parserParameters['mapperImProvider'] = $baseArray; | |
1554 | $parserParameters['mapperWebsiteType'] = $baseArray; | |
1555 | $parserParameters['mapperRelated'] = $baseArray; | |
1556 | $parserParameters['relatedContactType'] = $baseArray; | |
1557 | $parserParameters['relatedContactDetails'] = $baseArray; | |
1558 | $parserParameters['relatedContactLocType'] = $baseArray; | |
1559 | $parserParameters['relatedContactPhoneType'] = $baseArray; | |
1560 | $parserParameters['relatedContactImProvider'] = $baseArray; | |
1561 | $parserParameters['relatedContactWebsiteType'] = $baseArray; | |
1562 | ||
1563 | return $parserParameters; | |
1564 | ||
1565 | } | |
1566 | ||
64cafaa3 | 1567 | /** |
1568 | * Set field metadata. | |
1569 | */ | |
1570 | protected function setFieldMetadata() { | |
91b4c63e | 1571 | $this->setImportableFieldsMetadata($this->getContactImportMetadata()); |
64cafaa3 | 1572 | } |
1573 | ||
697f7b54 | 1574 | /** |
1575 | * @param array $newContact | |
697f7b54 | 1576 | * @param array $values |
1577 | * @param int $onDuplicate | |
1578 | * @param array $formatted | |
1579 | * @param array $contactFields | |
1580 | * | |
1581 | * @return int | |
1582 | * | |
1583 | * @throws \CRM_Core_Exception | |
1584 | * @throws \CiviCRM_API3_Exception | |
1585 | * @throws \Civi\API\Exception\UnauthorizedException | |
1586 | */ | |
970facdf | 1587 | private function handleDuplicateError(array $newContact, array $values, int $onDuplicate, array $formatted, array $contactFields): int { |
697f7b54 | 1588 | $urls = []; |
1589 | // need to fix at some stage and decide if the error will return an | |
1590 | // array or string, crude hack for now | |
1591 | if (is_array($newContact['error_message']['params'][0])) { | |
1592 | $cids = $newContact['error_message']['params'][0]; | |
1593 | } | |
1594 | else { | |
1595 | $cids = explode(',', $newContact['error_message']['params'][0]); | |
1596 | } | |
1597 | ||
1598 | foreach ($cids as $cid) { | |
1599 | $urls[] = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE); | |
1600 | } | |
1601 | ||
1602 | $url_string = implode("\n", $urls); | |
1603 | ||
1604 | // If we duplicate more than one record, skip no matter what | |
1605 | if (count($cids) > 1) { | |
1606 | $errorMessage = ts('Record duplicates multiple contacts'); | |
697f7b54 | 1607 | //combine error msg to avoid mismatch between error file columns. |
1608 | $errorMessage .= "\n" . $url_string; | |
1609 | array_unshift($values, $errorMessage); | |
12d121eb | 1610 | $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage); |
697f7b54 | 1611 | return CRM_Import_Parser::ERROR; |
1612 | } | |
1613 | ||
1614 | // Params only had one id, so shift it out | |
1615 | $contactId = array_shift($cids); | |
1616 | $cid = NULL; | |
1617 | ||
1618 | $vals = ['contact_id' => $contactId]; | |
3d603a3a | 1619 | if (in_array((int) $onDuplicate, [CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::DUPLICATE_FILL], TRUE)) { |
697f7b54 | 1620 | $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId); |
1621 | } | |
1622 | // else skip does nothing and just returns an error code. | |
1623 | if ($cid) { | |
1624 | $contact = [ | |
1625 | 'contact_id' => $cid, | |
1626 | ]; | |
1627 | $defaults = []; | |
1628 | $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults); | |
1629 | } | |
1630 | ||
eaedbd91 | 1631 | if (is_array($newContact)) { |
697f7b54 | 1632 | $contactID = $newContact['error_message']['params'][0]; |
1633 | if (is_array($contactID)) { | |
1634 | $contactID = array_pop($contactID); | |
1635 | } | |
1636 | if (!in_array($contactID, $this->_newContacts)) { | |
1637 | $this->_newContacts[] = $contactID; | |
1638 | } | |
1639 | } | |
1640 | //CRM-262 No Duplicate Checking | |
1641 | if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) { | |
1642 | array_unshift($values, $url_string); | |
12d121eb | 1643 | $this->setImportStatus((int) $values[count($values) - 1], 'DUPLICATE', 'Skipping duplicate record'); |
697f7b54 | 1644 | return CRM_Import_Parser::DUPLICATE; |
1645 | } | |
1646 | ||
12d121eb | 1647 | $this->setImportStatus((int) $values[count($values) - 1], 'Imported', ''); |
697f7b54 | 1648 | //return warning if street address is not parsed, CRM-5886 |
57d6aced | 1649 | return $this->processMessage($values, CRM_Import_Parser::VALID); |
697f7b54 | 1650 | } |
1651 | ||
68f3bda5 EM |
1652 | /** |
1653 | * Run import. | |
1654 | * | |
b6df6c1b EM |
1655 | * @param array $mapper Mapping as entered on MapField form. |
1656 | * e.g [['first_name']['email', 1]]. | |
1657 | * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput} | |
68f3bda5 | 1658 | * @param int $mode |
68f3bda5 | 1659 | * @param int $statusID |
68f3bda5 EM |
1660 | * |
1661 | * @return mixed | |
c169525f | 1662 | * @throws \API_Exception|\CRM_Core_Exception |
68f3bda5 EM |
1663 | */ |
1664 | public function run( | |
68f3bda5 EM |
1665 | $mapper = [], |
1666 | $mode = self::MODE_PREVIEW, | |
5e21b588 | 1667 | $statusID = NULL |
68f3bda5 EM |
1668 | ) { |
1669 | ||
1670 | // TODO: Make the timeout actually work | |
dba9288a EM |
1671 | $this->_onDuplicate = $onDuplicate = $this->getSubmittedValue('onDuplicate'); |
1672 | $this->_dedupeRuleGroupID = $this->getSubmittedValue('dedupe_rule_id'); | |
52bd01f5 EM |
1673 | // Since $this->_contactType is still being called directly do a get call |
1674 | // here to make sure it is instantiated. | |
1675 | $this->getContactType(); | |
80cb71bb | 1676 | $this->getContactSubType(); |
68f3bda5 EM |
1677 | |
1678 | $this->init(); | |
1679 | ||
06ef1cdc | 1680 | $this->_rowCount = 0; |
da8d3d49 | 1681 | $this->_totalCount = 0; |
68f3bda5 | 1682 | |
4a01628c EM |
1683 | $this->_primaryKeyName = '_id'; |
1684 | $this->_statusFieldName = '_status'; | |
68f3bda5 | 1685 | |
68f3bda5 EM |
1686 | if ($statusID) { |
1687 | $this->progressImport($statusID); | |
1688 | $startTimestamp = $currTimestamp = $prevTimestamp = time(); | |
1689 | } | |
5e21b588 EM |
1690 | $dataSource = $this->getDataSourceObject(); |
1691 | $totalRowCount = $dataSource->getRowCount(['new']); | |
68f3bda5 | 1692 | if ($mode == self::MODE_IMPORT) { |
5e21b588 | 1693 | $dataSource->setStatuses(['new']); |
68f3bda5 EM |
1694 | } |
1695 | ||
5e21b588 EM |
1696 | while ($row = $dataSource->getRow()) { |
1697 | $values = array_values($row); | |
68f3bda5 EM |
1698 | $this->_rowCount++; |
1699 | ||
68f3bda5 EM |
1700 | $this->_totalCount++; |
1701 | ||
f5d163df | 1702 | if ($mode == self::MODE_PREVIEW) { |
68f3bda5 EM |
1703 | $returnCode = $this->preview($values); |
1704 | } | |
1705 | elseif ($mode == self::MODE_SUMMARY) { | |
1706 | $returnCode = $this->summary($values); | |
1707 | } | |
1708 | elseif ($mode == self::MODE_IMPORT) { | |
52bd01f5 | 1709 | try { |
dba9288a | 1710 | $returnCode = $this->import($onDuplicate, $values); |
52bd01f5 EM |
1711 | } |
1712 | catch (CiviCRM_API3_Exception $e) { | |
1713 | // When we catch errors here we are not adding to the errors array - mostly | |
1714 | // because that will become obsolete once https://github.com/civicrm/civicrm-core/pull/23292 | |
1715 | // is merged and this will replace it as the main way to handle errors (ie. update the table | |
1716 | // and move on). | |
1717 | $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $e->getMessage()); | |
1718 | } | |
68f3bda5 EM |
1719 | if ($statusID && (($this->_rowCount % 50) == 0)) { |
1720 | $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount); | |
1721 | } | |
1722 | } | |
2940ddb3 EM |
1723 | // @todo this should be done within import - it probably is! |
1724 | if (isset($returnCode) && $returnCode === self::UNPARSED_ADDRESS_WARNING) { | |
c4f66023 | 1725 | $this->setImportStatus((int) $values[count($values) - 1], 'warning_unparsed_address', array_shift($values)); |
68f3bda5 EM |
1726 | } |
1727 | } | |
68f3bda5 EM |
1728 | } |
1729 | ||
1730 | /** | |
1731 | * Given a list of the importable field keys that the user has selected. | |
1732 | * set the active fields array to this list | |
1733 | * | |
1734 | * @param array $fieldKeys | |
1735 | * Mapped array of values. | |
1736 | */ | |
1737 | public function setActiveFields($fieldKeys) { | |
68f3bda5 EM |
1738 | foreach ($fieldKeys as $key) { |
1739 | if (empty($this->_fields[$key])) { | |
1740 | $this->_activeFields[] = new CRM_Contact_Import_Field('', ts('- do not import -')); | |
1741 | } | |
1742 | else { | |
1743 | $this->_activeFields[] = clone($this->_fields[$key]); | |
1744 | } | |
1745 | } | |
1746 | } | |
1747 | ||
68f3bda5 EM |
1748 | /** |
1749 | * Format the field values for input to the api. | |
1750 | * | |
5ebaab5d EM |
1751 | * @param array $values |
1752 | * The row from the datasource. | |
1753 | * | |
68f3bda5 | 1754 | * @return array |
5ebaab5d EM |
1755 | * Parameters mapped as described in getMappedRow |
1756 | * | |
4473c68d | 1757 | * @throws \API_Exception |
5ebaab5d EM |
1758 | * @todo - clean this up a bit & merge back into `getMappedRow` |
1759 | * | |
68f3bda5 | 1760 | */ |
5ebaab5d | 1761 | private function getParams(array $values): array { |
68f3bda5 | 1762 | $params = []; |
5ebaab5d | 1763 | |
4473c68d EM |
1764 | foreach ($this->getFieldMappings() as $i => $mappedField) { |
1765 | // The key is in the format 5_a_b where 5 is the relationship_type_id and a_b is the direction. | |
1766 | $relatedContactKey = $mappedField['relationship_type_id'] ? ($mappedField['relationship_type_id'] . '_' . $mappedField['relationship_direction']) : NULL; | |
748f4d17 | 1767 | $fieldName = $mappedField['name']; |
d8bf0d1a EM |
1768 | $importedValue = $values[$i]; |
1769 | if ($fieldName === 'do_not_import' || $importedValue === NULL) { | |
68f3bda5 EM |
1770 | continue; |
1771 | } | |
1772 | ||
f5d4a76c | 1773 | $locationFields = ['location_type_id', 'phone_type_id', 'provider_id', 'website_type_id']; |
7d03e8d6 | 1774 | $locationValues = array_filter(array_intersect_key($mappedField, array_fill_keys($locationFields, 1))); |
68f3bda5 | 1775 | |
7d03e8d6 EM |
1776 | if ($relatedContactKey) { |
1777 | if (!isset($params[$relatedContactKey])) { | |
77b23b82 EM |
1778 | $params[$relatedContactKey] = [ |
1779 | // These will be over-written by any the importer has chosen but defaults are based on the relationship. | |
1780 | 'contact_type' => $this->getRelatedContactType($mappedField['relationship_type_id'], $mappedField['relationship_direction']), | |
1781 | 'contact_sub_type' => $this->getRelatedContactSubType($mappedField['relationship_type_id'], $mappedField['relationship_direction']), | |
1782 | ]; | |
d8bf0d1a | 1783 | } |
19f33b09 | 1784 | $this->addFieldToParams($params[$relatedContactKey], $locationValues, $fieldName, $importedValue); |
d8bf0d1a EM |
1785 | } |
1786 | else { | |
19f33b09 | 1787 | $this->addFieldToParams($params, $locationValues, $fieldName, $importedValue); |
68f3bda5 EM |
1788 | } |
1789 | } | |
1790 | ||
018c9e26 EM |
1791 | $this->fillStateProvince($params); |
1792 | ||
68f3bda5 EM |
1793 | return $params; |
1794 | } | |
1795 | ||
1796 | /** | |
1797 | * @param string $name | |
1798 | * @param $title | |
1799 | * @param int $type | |
1800 | * @param string $headerPattern | |
1801 | * @param string $dataPattern | |
1802 | * @param bool $hasLocationType | |
1803 | */ | |
1804 | public function addField( | |
1805 | $name, $title, $type = CRM_Utils_Type::T_INT, | |
1806 | $headerPattern = '//', $dataPattern = '//', | |
1807 | $hasLocationType = FALSE | |
1808 | ) { | |
1809 | $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType); | |
1810 | if (empty($name)) { | |
1811 | $this->_fields['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType); | |
1812 | } | |
1813 | } | |
1814 | ||
1815 | /** | |
1816 | * Store parser values. | |
1817 | * | |
1818 | * @param CRM_Core_Session $store | |
1819 | * | |
1820 | * @param int $mode | |
1821 | */ | |
1822 | public function set($store, $mode = self::MODE_SUMMARY) { | |
68f3bda5 EM |
1823 | } |
1824 | ||
1825 | /** | |
1826 | * Export data to a CSV file. | |
1827 | * | |
1828 | * @param string $fileName | |
1829 | * @param array $header | |
1830 | * @param array $data | |
1831 | */ | |
1832 | public static function exportCSV($fileName, $header, $data) { | |
1833 | ||
1834 | if (file_exists($fileName) && !is_writable($fileName)) { | |
1835 | CRM_Core_Error::movedSiteError($fileName); | |
1836 | } | |
1837 | //hack to remove '_status', '_statusMsg' and '_id' from error file | |
1838 | $errorValues = []; | |
1839 | $dbRecordStatus = ['IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW']; | |
1840 | foreach ($data as $rowCount => $rowValues) { | |
1841 | $count = 0; | |
1842 | foreach ($rowValues as $key => $val) { | |
1843 | if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) { | |
1844 | break; | |
1845 | } | |
1846 | $errorValues[$rowCount][$key] = $val; | |
1847 | $count++; | |
1848 | } | |
1849 | } | |
1850 | $data = $errorValues; | |
1851 | ||
1852 | $output = []; | |
1853 | $fd = fopen($fileName, 'w'); | |
1854 | ||
1855 | foreach ($header as $key => $value) { | |
1856 | $header[$key] = "\"$value\""; | |
1857 | } | |
1858 | $config = CRM_Core_Config::singleton(); | |
1859 | $output[] = implode($config->fieldSeparator, $header); | |
1860 | ||
1861 | foreach ($data as $datum) { | |
1862 | foreach ($datum as $key => $value) { | |
1863 | $datum[$key] = "\"$value\""; | |
1864 | } | |
1865 | $output[] = implode($config->fieldSeparator, $datum); | |
1866 | } | |
1867 | fwrite($fd, implode("\n", $output)); | |
1868 | fclose($fd); | |
1869 | } | |
1870 | ||
68f3bda5 | 1871 | /** |
5e21b588 | 1872 | * Update the status of the import row to reflect the processing outcome. |
68f3bda5 EM |
1873 | * |
1874 | * @param int $id | |
1875 | * @param string $status | |
1876 | * @param string $message | |
5e21b588 EM |
1877 | * @param int|null $entityID |
1878 | * Optional created entity ID | |
1879 | * @param array $relatedEntityIDs | |
1880 | * Optional array e.g ['related_contact' => 4] | |
1881 | * | |
1882 | * @throws \API_Exception | |
1883 | * @throws \CRM_Core_Exception | |
68f3bda5 | 1884 | */ |
5e21b588 EM |
1885 | public function setImportStatus(int $id, string $status, string $message, ?int $entityID = NULL, array $relatedEntityIDs = []): void { |
1886 | $this->getDataSourceObject()->updateStatus($id, $status, $message, $entityID, $relatedEntityIDs); | |
68f3bda5 EM |
1887 | } |
1888 | ||
1889 | /** | |
1890 | * Format contact parameters. | |
1891 | * | |
1892 | * @todo this function needs re-writing & re-merging into the main function. | |
1893 | * | |
1894 | * Here be dragons. | |
1895 | * | |
1896 | * @param array $values | |
1897 | * @param array $params | |
1898 | * | |
1899 | * @return bool | |
1900 | */ | |
1901 | protected function formatContactParameters(&$values, &$params) { | |
1902 | // Crawl through the possible classes: | |
1903 | // Contact | |
1904 | // Individual | |
1905 | // Household | |
1906 | // Organization | |
1907 | // Location | |
1908 | // Address | |
1909 | ||
68f3bda5 EM |
1910 | // IM |
1911 | // Note | |
1912 | // Custom | |
1913 | ||
1914 | // first add core contact values since for other Civi modules they are not added | |
1915 | $contactFields = CRM_Contact_DAO_Contact::fields(); | |
1916 | _civicrm_api3_store_values($contactFields, $values, $params); | |
1917 | ||
1918 | if (isset($values['contact_type'])) { | |
1919 | // we're an individual/household/org property | |
1920 | ||
1921 | $fields[$values['contact_type']] = CRM_Contact_DAO_Contact::fields(); | |
1922 | ||
1923 | _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params); | |
1924 | return TRUE; | |
1925 | } | |
1926 | ||
1927 | // Cache the various object fields | |
1928 | // @todo - remove this after confirming this is just a compilation of other-wise-cached fields. | |
1929 | static $fields = []; | |
1930 | ||
68f3bda5 EM |
1931 | if (isset($values['note'])) { |
1932 | // add a note field | |
1933 | if (!isset($params['note'])) { | |
1934 | $params['note'] = []; | |
1935 | } | |
1936 | $noteBlock = count($params['note']) + 1; | |
1937 | ||
1938 | $params['note'][$noteBlock] = []; | |
1939 | if (!isset($fields['Note'])) { | |
1940 | $fields['Note'] = CRM_Core_DAO_Note::fields(); | |
1941 | } | |
1942 | ||
1943 | // get the current logged in civicrm user | |
1944 | $session = CRM_Core_Session::singleton(); | |
1945 | $userID = $session->get('userID'); | |
1946 | ||
1947 | if ($userID) { | |
1948 | $values['contact_id'] = $userID; | |
1949 | } | |
1950 | ||
1951 | _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]); | |
1952 | ||
1953 | return TRUE; | |
1954 | } | |
1955 | ||
1956 | // Check for custom field values | |
1957 | $customFields = CRM_Core_BAO_CustomField::getFields(CRM_Utils_Array::value('contact_type', $values), | |
1958 | FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE | |
1959 | ); | |
1960 | ||
1961 | foreach ($values as $key => $value) { | |
1962 | if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) { | |
1963 | // check if it's a valid custom field id | |
1964 | ||
1965 | if (!array_key_exists($customFieldID, $customFields)) { | |
1966 | return civicrm_api3_create_error('Invalid custom field ID'); | |
1967 | } | |
1968 | else { | |
1969 | $params[$key] = $value; | |
1970 | } | |
1971 | } | |
1972 | } | |
1973 | return TRUE; | |
1974 | } | |
1975 | ||
1976 | /** | |
1977 | * Format location block ready for importing. | |
1978 | * | |
639e4f37 EM |
1979 | * There is some test coverage for this in |
1980 | * CRM_Contact_Import_Parser_ContactTest e.g. testImportPrimaryAddress. | |
68f3bda5 EM |
1981 | * |
1982 | * @param array $values | |
1983 | * @param array $params | |
1984 | * | |
1985 | * @return bool | |
639e4f37 | 1986 | * @throws \CiviCRM_API3_Exception |
68f3bda5 EM |
1987 | */ |
1988 | protected function formatLocationBlock(&$values, &$params) { | |
68f3bda5 EM |
1989 | // Note: we doing multiple value formatting here for address custom fields, plus putting into right format. |
1990 | // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving | |
1991 | // the address in CRM_Core_BAO_Address::create method | |
1992 | if (!empty($values['location_type_id'])) { | |
1993 | static $customFields = []; | |
1994 | if (empty($customFields)) { | |
1995 | $customFields = CRM_Core_BAO_CustomField::getFields('Address'); | |
1996 | } | |
1997 | // make a copy of values, as we going to make changes | |
1998 | $newValues = $values; | |
1999 | foreach ($values as $key => $val) { | |
2000 | $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key); | |
2001 | if ($customFieldID && array_key_exists($customFieldID, $customFields)) { | |
2002 | ||
2003 | $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL; | |
2004 | if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]) && $val) { | |
2005 | $mulValues = explode(',', $val); | |
2006 | $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE); | |
2007 | $newValues[$key] = []; | |
2008 | foreach ($mulValues as $v1) { | |
2009 | foreach ($customOption as $v2) { | |
2010 | if ((strtolower($v2['label']) == strtolower(trim($v1))) || | |
2011 | (strtolower($v2['value']) == strtolower(trim($v1))) | |
2012 | ) { | |
2013 | if ($htmlType == 'CheckBox') { | |
2014 | $newValues[$key][$v2['value']] = 1; | |
2015 | } | |
2016 | else { | |
2017 | $newValues[$key][] = $v2['value']; | |
2018 | } | |
2019 | } | |
2020 | } | |
2021 | } | |
2022 | } | |
2023 | } | |
2024 | } | |
2025 | // consider new values | |
2026 | $values = $newValues; | |
2027 | } | |
2028 | ||
68f3bda5 EM |
2029 | return TRUE; |
2030 | } | |
2031 | ||
2032 | /** | |
2033 | * Get the field metadata for the relevant entity. | |
2034 | * | |
2035 | * @param string $entity | |
2036 | * | |
2037 | * @return array | |
2038 | */ | |
2039 | protected function getMetadataForEntity($entity) { | |
2040 | if (!isset($this->fieldMetadata[$entity])) { | |
2041 | $className = "CRM_Core_DAO_$entity"; | |
2042 | $this->fieldMetadata[$entity] = $className::fields(); | |
2043 | } | |
2044 | return $this->fieldMetadata[$entity]; | |
2045 | } | |
2046 | ||
2047 | /** | |
2048 | * Fill in the primary location. | |
2049 | * | |
2050 | * If the contact has a primary address we update it. Otherwise | |
2051 | * we add an address of the default location type. | |
2052 | * | |
2053 | * @param array $params | |
2054 | * Address block parameters | |
2055 | * @param array $values | |
2056 | * Input values | |
2057 | * @param string $entity | |
2058 | * - address, email, phone | |
2059 | * @param int|null $contactID | |
2060 | * | |
2061 | * @throws \CiviCRM_API3_Exception | |
2062 | */ | |
2063 | protected function fillPrimary(&$params, $values, $entity, $contactID) { | |
2064 | if ($values['location_type_id'] === 'Primary') { | |
2065 | if ($contactID) { | |
2066 | $primary = civicrm_api3($entity, 'get', [ | |
2067 | 'return' => 'location_type_id', | |
2068 | 'contact_id' => $contactID, | |
2069 | 'is_primary' => 1, | |
2070 | 'sequential' => 1, | |
2071 | ]); | |
2072 | } | |
2073 | $defaultLocationType = CRM_Core_BAO_LocationType::getDefault(); | |
2074 | $params['location_type_id'] = (int) (isset($primary) && $primary['count']) ? $primary['values'][0]['location_type_id'] : $defaultLocationType->id; | |
2075 | $params['is_primary'] = 1; | |
2076 | } | |
2077 | } | |
2078 | ||
34f3f22a EM |
2079 | /** |
2080 | * Get the civicrm_mapping_field appropriate layout for the mapper input. | |
2081 | * | |
2082 | * The input looks something like ['street_address', 1] | |
2083 | * and would be mapped to ['name' => 'street_address', 'location_type_id' => | |
2084 | * 1] | |
2085 | * | |
2086 | * @param array $fieldMapping | |
b6df6c1b EM |
2087 | * Field as submitted on the MapField form - this is a non-associative array, |
2088 | * the keys of which depend on the data/ field. Generally it will be one of | |
2089 | * [$fieldName], | |
2090 | * [$fieldName, $locationTypeID, $phoneTypeIDOrIMProviderIDIfRelevant], | |
2091 | * [$fieldName, $websiteTypeID], | |
2092 | * If the mapping is for a related contact it will be as above but the first | |
2093 | * key will be the relationship key - eg. 5_a_b. | |
34f3f22a EM |
2094 | * @param int $mappingID |
2095 | * @param int $columnNumber | |
2096 | * | |
2097 | * @return array | |
2098 | * @throws \API_Exception | |
2099 | */ | |
2100 | public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array { | |
2101 | $isRelationshipField = preg_match('/\d*_a_b|b_a$/', $fieldMapping[0]); | |
2102 | $fieldName = $isRelationshipField ? $fieldMapping[1] : $fieldMapping[0]; | |
2103 | $locationTypeID = NULL; | |
2104 | $possibleLocationField = $isRelationshipField ? 2 : 1; | |
639e4f37 EM |
2105 | $entity = strtolower($this->getFieldEntity($fieldName)); |
2106 | if ($entity !== 'website' && is_numeric($fieldMapping[$possibleLocationField] ?? NULL)) { | |
34f3f22a EM |
2107 | $locationTypeID = $fieldMapping[$possibleLocationField]; |
2108 | } | |
639e4f37 | 2109 | |
34f3f22a EM |
2110 | return [ |
2111 | 'name' => $fieldName, | |
2112 | 'mapping_id' => $mappingID, | |
2113 | 'relationship_type_id' => $isRelationshipField ? substr($fieldMapping[0], 0, -4) : NULL, | |
2114 | 'relationship_direction' => $isRelationshipField ? substr($fieldMapping[0], -3) : NULL, | |
2115 | 'column_number' => $columnNumber, | |
2116 | 'contact_type' => $this->getContactType(), | |
639e4f37 EM |
2117 | 'website_type_id' => $entity !== 'website' ? NULL : ($isRelationshipField ? $fieldMapping[2] : $fieldMapping[1]), |
2118 | 'phone_type_id' => $entity !== 'phone' ? NULL : ($isRelationshipField ? $fieldMapping[3] : $fieldMapping[2]), | |
2119 | 'im_provider_id' => $entity !== 'im' ? NULL : ($isRelationshipField ? $fieldMapping[3] : $fieldMapping[2]), | |
34f3f22a EM |
2120 | 'location_type_id' => $locationTypeID, |
2121 | ]; | |
2122 | } | |
2123 | ||
2124 | /** | |
2125 | * @param array $mappedField | |
2126 | * Field detail as would be saved in field_mapping table | |
2127 | * or as returned from getMappingFieldFromMapperInput | |
2128 | * | |
2129 | * @return string | |
2130 | * @throws \API_Exception | |
2131 | */ | |
2132 | public function getMappedFieldLabel(array $mappedField): string { | |
2133 | $this->setFieldMetadata(); | |
2134 | $title = []; | |
2135 | if ($mappedField['relationship_type_id']) { | |
2136 | $title[] = $this->getRelationshipLabel($mappedField['relationship_type_id'], $mappedField['relationship_direction']); | |
2137 | } | |
e0b8f9a9 | 2138 | $title[] = $this->getFieldMetadata($mappedField['name'])['title']; |
34f3f22a EM |
2139 | if ($mappedField['location_type_id']) { |
2140 | $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Address', 'location_type_id', $mappedField['location_type_id']); | |
2141 | } | |
2142 | if ($mappedField['website_type_id']) { | |
2143 | $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Website', 'website_type_id', $mappedField['website_type_id']); | |
2144 | } | |
2145 | if ($mappedField['phone_type_id']) { | |
2146 | $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']); | |
2147 | } | |
2148 | if ($mappedField['im_provider_id']) { | |
0e38b3e4 | 2149 | $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['im_provider_id']); |
34f3f22a EM |
2150 | } |
2151 | return implode(' - ', $title); | |
2152 | } | |
2153 | ||
2154 | /** | |
2155 | * Get the relevant label for the relationship. | |
2156 | * | |
2157 | * @param int $id | |
2158 | * @param string $direction | |
2159 | * | |
2160 | * @return string | |
2161 | * @throws \API_Exception | |
2162 | */ | |
2163 | protected function getRelationshipLabel(int $id, string $direction): string { | |
2164 | if (empty($this->relationshipLabels[$id . $direction])) { | |
2165 | $this->relationshipLabels[$id . $direction] = | |
2166 | $fieldName = 'label_' . $direction; | |
2167 | $this->relationshipLabels[$id . $direction] = (string) RelationshipType::get(FALSE) | |
2168 | ->addWhere('id', '=', $id) | |
2169 | ->addSelect($fieldName)->execute()->first()[$fieldName]; | |
2170 | } | |
2171 | return $this->relationshipLabels[$id . $direction]; | |
2172 | } | |
2173 | ||
4b3ef371 | 2174 | /** |
baeb2d67 | 2175 | * Transform the input parameters into the form handled by the input routine. |
4b3ef371 EM |
2176 | * |
2177 | * @param array $values | |
2178 | * Input parameters as they come in from the datasource | |
2179 | * eg. ['Bob', 'Smith', 'bob@example.org', '123-456'] | |
2180 | * | |
2181 | * @return array | |
2182 | * Parameters mapped to CiviCRM fields based on the mapping | |
2183 | * and specified contact type. eg. | |
2184 | * [ | |
2185 | * 'contact_type' => 'Individual', | |
2186 | * 'first_name' => 'Bob', | |
2187 | * 'last_name' => 'Smith', | |
2188 | * 'phone' => ['phone' => '123', 'location_type_id' => 1, 'phone_type_id' => 1], | |
2189 | * '5_a_b' => ['contact_type' => 'Organization', 'url' => ['url' => 'https://example.org', 'website_type_id' => 1]] | |
2190 | * 'im' => ['im' => 'my-handle', 'location_type_id' => 1, 'provider_id' => 1], | |
baeb2d67 EM |
2191 | * |
2192 | * @throws \API_Exception | |
4b3ef371 EM |
2193 | */ |
2194 | public function getMappedRow(array $values): array { | |
5ebaab5d | 2195 | $params = $this->getParams($values); |
baeb2d67 | 2196 | $params['contact_type'] = $this->getContactType(); |
e0b8f9a9 EM |
2197 | if ($this->getContactSubType()) { |
2198 | $params['contact_sub_type'] = $this->getContactSubType(); | |
2199 | } | |
4b3ef371 EM |
2200 | return $params; |
2201 | } | |
2202 | ||
13575591 EM |
2203 | /** |
2204 | * Validate the import values. | |
2205 | * | |
2206 | * The values array represents a row in the datasource. | |
2207 | * | |
2208 | * @param array $values | |
5ebaab5d EM |
2209 | * |
2210 | * @throws \API_Exception | |
7e56b830 | 2211 | * @throws \CRM_Core_Exception |
13575591 EM |
2212 | */ |
2213 | public function validateValues(array $values): void { | |
13575591 | 2214 | $params = $this->getMappedRow($values); |
02f1858b | 2215 | $contacts = array_merge(['0' => $params], $this->getRelatedContactsParams($params)); |
24948d41 | 2216 | $errors = []; |
02f1858b EM |
2217 | foreach ($contacts as $value) { |
2218 | // If we are referencing a related contact, or are in update mode then we | |
2219 | // don't need all the required fields if we have enough to find an existing contact. | |
2220 | $useExistingMatchFields = !empty($value['relationship_type_id']) || $this->isUpdateExistingContacts(); | |
24948d41 EM |
2221 | $prefixString = !empty($value['relationship_label']) ? '(' . $value['relationship_label'] . ') ' : ''; |
2222 | $this->validateRequiredContactFields($value['contact_type'], $value, $useExistingMatchFields, $prefixString); | |
77b23b82 | 2223 | |
24948d41 | 2224 | $errors = array_merge($errors, $this->getInvalidValuesForContact($value, $prefixString)); |
77b23b82 EM |
2225 | if (!empty($value['relationship_type_id'])) { |
2226 | $requiredSubType = $this->getRelatedContactSubType($value['relationship_type_id'], $value['relationship_direction']); | |
2227 | if ($requiredSubType && $value['contact_sub_type'] && $requiredSubType !== $value['contact_sub_type']) { | |
2228 | throw new CRM_Core_Exception($prefixString . ts('Mismatched or Invalid contact subtype found for this related contact.')); | |
2229 | } | |
2230 | } | |
13575591 EM |
2231 | } |
2232 | ||
2233 | //check for duplicate external Identifier | |
7e56b830 | 2234 | $externalID = $params['external_identifier'] ?? NULL; |
13575591 EM |
2235 | if ($externalID) { |
2236 | /* If it's a dupe,external Identifier */ | |
2237 | ||
2238 | if ($externalDupe = CRM_Utils_Array::value($externalID, $this->_allExternalIdentifiers)) { | |
2239 | $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]); | |
2240 | throw new CRM_Core_Exception($errorMessage); | |
2241 | } | |
2242 | //otherwise, count it and move on | |
2243 | $this->_allExternalIdentifiers[$externalID] = $this->_lineCount; | |
2244 | } | |
2245 | ||
2246 | //date-format part ends | |
2247 | ||
24948d41 | 2248 | $errorMessage = implode(', ', $errors); |
13575591 EM |
2249 | |
2250 | //checking error in core data | |
2251 | $this->isErrorInCoreData($params, $errorMessage); | |
2252 | if ($errorMessage) { | |
2253 | $tempMsg = "Invalid value for field(s) : $errorMessage"; | |
2254 | throw new CRM_Core_Exception($tempMsg); | |
2255 | } | |
2256 | } | |
2257 | ||
24948d41 EM |
2258 | /** |
2259 | * Get the invalid values in the params for the given contact. | |
2260 | * | |
2261 | * @param array|int|string $value | |
2262 | * @param string $prefixString | |
2263 | * | |
2264 | * @return array | |
2265 | * @throws \API_Exception | |
2266 | * @throws \Civi\API\Exception\NotImplementedException | |
2267 | */ | |
2268 | protected function getInvalidValuesForContact($value, string $prefixString): array { | |
2269 | $errors = []; | |
2270 | foreach ($value as $contactKey => $contactValue) { | |
2271 | if (!preg_match('/^\d+_[a|b]_[a|b]$/', $contactKey)) { | |
2272 | $result = $this->getInvalidValues($contactValue, $contactKey, $prefixString); | |
2273 | if (!empty($result)) { | |
2274 | $errors = array_merge($errors, $result); | |
2275 | } | |
2276 | } | |
2277 | } | |
2278 | return $errors; | |
2279 | } | |
2280 | ||
4473c68d EM |
2281 | /** |
2282 | * Get the field mappings for the import. | |
2283 | * | |
2284 | * This is the same format as saved in civicrm_mapping_field except | |
2285 | * that location_type_id = 'Primary' rather than empty where relevant. | |
f5d4a76c | 2286 | * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id' |
4473c68d EM |
2287 | * |
2288 | * @return array | |
2289 | * @throws \API_Exception | |
2290 | */ | |
2291 | protected function getFieldMappings(): array { | |
2292 | $mappedFields = []; | |
2293 | foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) { | |
2294 | $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i); | |
2295 | if (!$mappedField['location_type_id'] && !empty($this->importableFieldsMetadata[$mappedField['name']]['hasLocationType'])) { | |
2296 | $mappedField['location_type_id'] = 'Primary'; | |
2297 | } | |
f5d4a76c | 2298 | // Just for clarity since 0 is a pseudo-value |
4473c68d | 2299 | unset($mappedField['mapping_id']); |
f5d4a76c EM |
2300 | // Annoyingly the civicrm_mapping_field name for this differs from civicrm_im. |
2301 | // Test cover in `CRM_Contact_Import_Parser_ContactTest::testMapFields` | |
2302 | $mappedField['provider_id'] = $mappedField['im_provider_id']; | |
2303 | unset($mappedField['im_provider_id']); | |
4473c68d EM |
2304 | $mappedFields[] = $mappedField; |
2305 | } | |
2306 | return $mappedFields; | |
2307 | } | |
2308 | ||
40a48df0 EM |
2309 | /** |
2310 | * Get the related contact type. | |
2311 | * | |
2312 | * @param int|null $relationshipTypeID | |
2313 | * @param int|string $relationshipDirection | |
2314 | * | |
2315 | * @return null|string | |
2316 | * | |
2317 | * @throws \API_Exception | |
2318 | */ | |
2319 | protected function getRelatedContactType($relationshipTypeID, $relationshipDirection): ?string { | |
2320 | if (!$relationshipTypeID) { | |
2321 | return NULL; | |
2322 | } | |
7d2012dc | 2323 | $relationshipField = 'contact_type_' . substr($relationshipDirection, -1); |
02f1858b | 2324 | return $this->getRelationshipType($relationshipTypeID)[$relationshipField]; |
7d2012dc EM |
2325 | } |
2326 | ||
77b23b82 EM |
2327 | /** |
2328 | * Get the related contact sub type. | |
2329 | * | |
2330 | * @param int|null $relationshipTypeID | |
2331 | * @param int|string $relationshipDirection | |
2332 | * | |
2333 | * @return null|string | |
2334 | * | |
2335 | * @throws \API_Exception | |
2336 | */ | |
2337 | protected function getRelatedContactSubType(int $relationshipTypeID, $relationshipDirection): ?string { | |
2338 | if (!$relationshipTypeID) { | |
2339 | return NULL; | |
2340 | } | |
2341 | $relationshipField = 'contact_sub_type_' . substr($relationshipDirection, -1); | |
2342 | return $this->getRelationshipType($relationshipTypeID)[$relationshipField]; | |
2343 | } | |
2344 | ||
7d2012dc EM |
2345 | /** |
2346 | * Get the related contact type. | |
2347 | * | |
2348 | * @param int|null $relationshipTypeID | |
2349 | * @param int|string $relationshipDirection | |
2350 | * | |
2351 | * @return null|string | |
2352 | * | |
2353 | * @throws \API_Exception | |
2354 | */ | |
2355 | protected function getRelatedContactLabel($relationshipTypeID, $relationshipDirection): ?string { | |
2356 | $relationshipField = 'label_' . $relationshipDirection; | |
02f1858b | 2357 | return $this->getRelationshipType($relationshipTypeID)[$relationshipField]; |
7d2012dc EM |
2358 | } |
2359 | ||
2360 | /** | |
2361 | * Get the relationship type. | |
2362 | * | |
2363 | * @param int $relationshipTypeID | |
2364 | * | |
2365 | * @return string[] | |
2366 | * @throws \API_Exception | |
2367 | */ | |
2368 | protected function getRelationshipType(int $relationshipTypeID): array { | |
2369 | $cacheKey = 'relationship_type' . $relationshipTypeID; | |
40a48df0 | 2370 | if (!isset(Civi::$statics[__CLASS__][$cacheKey])) { |
40a48df0 EM |
2371 | Civi::$statics[__CLASS__][$cacheKey] = RelationshipType::get(FALSE) |
2372 | ->addWhere('id', '=', $relationshipTypeID) | |
7d2012dc | 2373 | ->addSelect('*')->execute()->first(); |
40a48df0 EM |
2374 | } |
2375 | return Civi::$statics[__CLASS__][$cacheKey]; | |
2376 | } | |
2377 | ||
19f33b09 EM |
2378 | /** |
2379 | * Add the given field to the contact array. | |
2380 | * | |
2381 | * @param array $contactArray | |
2382 | * @param array $locationValues | |
2383 | * @param string $fieldName | |
2384 | * @param mixed $importedValue | |
2385 | * | |
2386 | * @return void | |
2387 | * @throws \API_Exception | |
2388 | */ | |
2389 | private function addFieldToParams(array &$contactArray, array $locationValues, string $fieldName, $importedValue): void { | |
2390 | if (!empty($locationValues)) { | |
018c9e26 | 2391 | $fieldMap = ['country' => 'country_id', 'state_province' => 'state_province_id', 'county' => 'county_id']; |
24948d41 EM |
2392 | $realFieldName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName]; |
2393 | $entity = strtolower($this->getFieldEntity($fieldName)); | |
639e4f37 | 2394 | |
24948d41 EM |
2395 | // The entity key is either location_type_id for address, email - eg. 1, or |
2396 | // location_type_id + '_' + phone_type_id or im_provider_id | |
2397 | // or the value for website(since websites are not historically one-per-type) | |
2398 | $entityKey = $locationValues['location_type_id'] ?? $importedValue; | |
2399 | if (!empty($locationValues['phone_type_id']) || !empty($locationValues['provider_id'])) { | |
2400 | $entityKey .= '_' . ($locationValues['phone_type_id'] ?? '' . $locationValues['provider_id'] ?? ''); | |
2401 | } | |
2402 | $fieldValue = $this->getTransformedFieldValue($realFieldName, $importedValue); | |
639e4f37 | 2403 | |
24948d41 EM |
2404 | if (!isset($contactArray[$entity][$entityKey])) { |
2405 | $contactArray[$entity][$entityKey] = $locationValues; | |
2406 | } | |
018c9e26 EM |
2407 | // So im has really non-standard handling... |
2408 | $reallyRealFieldName = $realFieldName === 'im' ? 'name' : $realFieldName; | |
639e4f37 | 2409 | $contactArray[$entity][$entityKey][$reallyRealFieldName] = $fieldValue; |
19f33b09 EM |
2410 | } |
2411 | else { | |
80e9f1a2 | 2412 | $fieldName = array_search($fieldName, $this->getOddlyMappedMetadataFields(), TRUE) ?: $fieldName; |
19f33b09 EM |
2413 | $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue); |
2414 | } | |
2415 | } | |
2416 | ||
02f1858b EM |
2417 | /** |
2418 | * Get any related contacts designated for update. | |
2419 | * | |
2420 | * This extracts the parts that relate to separate related | |
2421 | * contacts from the 'params' array. | |
2422 | * | |
2423 | * It is probably a bit silly not to nest them more clearly in | |
2424 | * `getParams` in the first place & maybe in future we can do that. | |
2425 | * | |
2426 | * @param array $params | |
2427 | * | |
2428 | * @return array | |
2429 | * e.g ['5_a_b' => ['contact_type' => 'Organization', 'organization_name' => 'The Firm']] | |
2430 | * @throws \API_Exception | |
2431 | */ | |
2432 | protected function getRelatedContactsParams(array $params): array { | |
2433 | $relatedContacts = []; | |
2434 | foreach ($params as $key => $value) { | |
2435 | // If the key is a relationship key - eg. 5_a_b or 10_b_a | |
2436 | // then the value is an array that describes an existing contact. | |
2437 | // We need to check the fields are present to identify or create this | |
2438 | // contact. | |
2439 | if (preg_match('/^\d+_[a|b]_[a|b]$/', $key)) { | |
2440 | $value['relationship_type_id'] = substr($key, 0, -4); | |
2441 | $value['relationship_direction'] = substr($key, -3); | |
2442 | $value['relationship_label'] = $this->getRelationshipLabel($value['relationship_type_id'], $value['relationship_direction']); | |
2443 | $relatedContacts[$key] = $value; | |
2444 | } | |
2445 | } | |
2446 | return $relatedContacts; | |
2447 | } | |
2448 | ||
64623d6c EM |
2449 | /** |
2450 | * Look up for an existing contact with the given external_identifier. | |
2451 | * | |
2452 | * If the identifier is found on a deleted contact then it is not a match | |
2453 | * but it must be removed from that contact to allow the new contact to | |
2454 | * have that external_identifier. | |
2455 | * | |
2456 | * @param string|null $externalIdentifier | |
ed75aff2 | 2457 | * @param string $contactType |
64623d6c EM |
2458 | * |
2459 | * @return int|null | |
2460 | * | |
ed75aff2 | 2461 | * @throws \CRM_Core_Exception |
64623d6c EM |
2462 | * @throws \CiviCRM_API3_Exception |
2463 | */ | |
ed75aff2 | 2464 | protected function lookupExternalIdentifier(?string $externalIdentifier, string $contactType): ?int { |
64623d6c EM |
2465 | if (!$externalIdentifier) { |
2466 | return NULL; | |
2467 | } | |
2468 | // Check for any match on external id, deleted or otherwise. | |
2469 | $foundContact = civicrm_api3('Contact', 'get', [ | |
2470 | 'external_identifier' => $externalIdentifier, | |
2471 | 'showAll' => 'all', | |
2472 | 'sequential' => TRUE, | |
ed75aff2 | 2473 | 'return' => ['id', 'contact_is_deleted', 'contact_type'], |
64623d6c EM |
2474 | ]); |
2475 | if (empty($foundContact['id'])) { | |
2476 | return NULL; | |
2477 | } | |
2478 | if (!empty($foundContact['values'][0]['contact_is_deleted'])) { | |
2479 | // If the contact is deleted, update external identifier to be blank | |
2480 | // to avoid key error from MySQL. | |
2481 | $params = ['id' => $foundContact['id'], 'external_identifier' => '']; | |
2482 | civicrm_api3('Contact', 'create', $params); | |
2483 | return NULL; | |
2484 | } | |
ed75aff2 EM |
2485 | if ($foundContact['values'][0]['contact_type'] !== $contactType) { |
2486 | throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH); | |
2487 | } | |
64623d6c EM |
2488 | return (int) $foundContact['id']; |
2489 | } | |
2490 | ||
2491 | /** | |
2492 | * Lookup the contact's contact ID. | |
2493 | * | |
2494 | * @param array $params | |
2495 | * @param bool $isDuplicateIfExternalIdentifierExists | |
2496 | * | |
2497 | * @return int|null | |
2498 | * | |
2499 | * @throws \API_Exception | |
2500 | * @throws \CRM_Core_Exception | |
2501 | * @throws \CiviCRM_API3_Exception | |
2502 | */ | |
2503 | protected function lookupContactID(array $params, bool $isDuplicateIfExternalIdentifierExists): ?int { | |
ed75aff2 | 2504 | $extIDMatch = $this->lookupExternalIdentifier($params['external_identifier'] ?? NULL, $params['contact_type']); |
77b23b82 EM |
2505 | if (!empty($params['external_identifier']) && !$extIDMatch && $isDuplicateIfExternalIdentifierExists) { |
2506 | throw new CRM_Core_Exception(ts('Existing external ID lookup failed.'), CRM_Import_Parser::ERROR); | |
2507 | } | |
64623d6c EM |
2508 | $contactID = !empty($params['id']) ? (int) $params['id'] : NULL; |
2509 | //check if external identifier exists in database | |
2510 | if ($extIDMatch && $contactID && $extIDMatch !== $contactID) { | |
2511 | throw new CRM_Core_Exception(ts('Existing external ID does not match the imported contact ID.'), CRM_Import_Parser::ERROR); | |
2512 | } | |
2513 | if ($extIDMatch && $isDuplicateIfExternalIdentifierExists) { | |
2514 | throw new CRM_Core_Exception(ts('External ID already exists in Database.'), CRM_Import_Parser::DUPLICATE); | |
2515 | } | |
2516 | if ($contactID) { | |
ed75aff2 EM |
2517 | $existingContact = Contact::get(FALSE) |
2518 | ->addWhere('id', '=', $contactID) | |
2519 | // Don't auto-filter deleted - people use import to undelete. | |
2520 | ->addWhere('is_deleted', 'IN', [0, 1]) | |
2521 | ->addSelect('contact_type')->execute()->first(); | |
2522 | if (empty($existingContact['id'])) { | |
2523 | throw new CRM_Core_Exception('No contact found for this contact ID:' . $params['id'], CRM_Import_Parser::NO_MATCH); | |
2524 | } | |
2525 | if ($existingContact['contact_type'] !== $params['contact_type']) { | |
2526 | throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH); | |
2527 | } | |
64623d6c EM |
2528 | return $contactID; |
2529 | } | |
2530 | // Time to see if we can find an existing contact ID to make this an update | |
2531 | // not a create. | |
2532 | if ($extIDMatch || $this->isUpdateExistingContacts()) { | |
b011a75f | 2533 | return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id') ?: NULL); |
64623d6c EM |
2534 | } |
2535 | return NULL; | |
2536 | } | |
2537 | ||
77b23b82 EM |
2538 | /** |
2539 | * @param array $params | |
2540 | * @param array $formatted | |
2541 | * @return array[] | |
2542 | * @throws \API_Exception | |
2543 | * @throws \CRM_Core_Exception | |
2544 | * @throws \CiviCRM_API3_Exception | |
2545 | * @throws \Civi\API\Exception\UnauthorizedException | |
2546 | */ | |
2547 | protected function processContact(array $params, array $formatted): array { | |
2548 | $params['id'] = $formatted['id'] = $this->lookupContactID($params, ($this->isSkipDuplicates() || $this->isIgnoreDuplicates())); | |
2549 | if ($params['id'] && $params['contact_sub_type']) { | |
2550 | $contactSubType = Contact::get(FALSE) | |
2551 | ->addWhere('id', '=', $params['id']) | |
2552 | ->addSelect('contact_sub_type') | |
2553 | ->execute() | |
2554 | ->first()['contact_sub_type']; | |
2555 | if (!empty($contactSubType) && $contactSubType[0] !== $params['contact_sub_type'] && !CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType[0])) { | |
2556 | throw new CRM_Core_Exception('Mismatched contact SubTypes :', CRM_Import_Parser::NO_MATCH); | |
2557 | } | |
2558 | } | |
2559 | return array($formatted, $params); | |
2560 | } | |
2561 | ||
018c9e26 EM |
2562 | /** |
2563 | * Try to get the correct state province using what country information we have. | |
2564 | * | |
2565 | * If the state matches more than one possibility then either the imported | |
2566 | * country of the site country should help us.... | |
2567 | * | |
2568 | * @param string $stateProvince | |
2569 | * @param int|null|string $countryID | |
2570 | * | |
2571 | * @return int|string | |
2572 | * @throws \API_Exception | |
2573 | * @throws \Civi\API\Exception\UnauthorizedException | |
2574 | */ | |
2575 | private function tryToResolveStateProvince(string $stateProvince, $countryID) { | |
2576 | // Try to disambiguate since we likely have the country now. | |
2577 | $possibleStates = $this->ambiguousOptions['state_province_id'][mb_strtolower($stateProvince)]; | |
2578 | if ($countryID) { | |
2579 | return $this->checkStatesForCountry($countryID, $possibleStates) ?: 'invalid_import_value'; | |
2580 | } | |
2581 | // Try the default country next. | |
2582 | $defaultCountryMatch = $this->checkStatesForCountry($this->getSiteDefaultCountry(), $possibleStates); | |
2583 | if ($defaultCountryMatch) { | |
2584 | return $defaultCountryMatch; | |
2585 | } | |
2586 | ||
2587 | if ($this->getAvailableCountries()) { | |
2588 | $countryMatches = []; | |
2589 | foreach ($this->getAvailableCountries() as $availableCountryID) { | |
2590 | $possible = $this->checkStatesForCountry($availableCountryID, $possibleStates); | |
2591 | if ($possible) { | |
2592 | $countryMatches[] = $possible; | |
2593 | } | |
2594 | } | |
2595 | if (count($countryMatches) === 1) { | |
2596 | return reset($countryMatches); | |
2597 | } | |
2598 | ||
2599 | } | |
2600 | return $stateProvince; | |
2601 | } | |
2602 | ||
2603 | /** | |
2604 | * @param array $params | |
2605 | * | |
2606 | * @return array | |
2607 | * @throws \API_Exception | |
2608 | */ | |
2609 | private function fillStateProvince(array &$params): array { | |
2610 | foreach ($params as $key => $value) { | |
2611 | if ($key === 'address') { | |
2612 | foreach ($value as $index => $address) { | |
2613 | $stateProvinceID = $address['state_province_id'] ?? NULL; | |
2614 | if ($stateProvinceID) { | |
2615 | if (!is_numeric($stateProvinceID)) { | |
2616 | $params['address'][$index]['state_province_id'] = $this->tryToResolveStateProvince($stateProvinceID, $address['country_id'] ?? NULL); | |
2617 | } | |
2618 | elseif (!empty($address['country_id']) && is_numeric($address['country_id'])) { | |
2619 | if (!$this->checkStatesForCountry((int) $address['country_id'], [$stateProvinceID])) { | |
2620 | $params['address'][$index]['state_province_id'] = 'invalid_import_value'; | |
2621 | } | |
2622 | } | |
2623 | } | |
2624 | } | |
2625 | } | |
2626 | elseif (is_array($value) && !in_array($key, ['email', 'phone', 'im', 'website', 'openid'], TRUE)) { | |
2627 | $this->fillStateProvince($params[$key]); | |
2628 | } | |
2629 | } | |
2630 | return $params; | |
2631 | } | |
2632 | ||
2633 | /** | |
2634 | * Check is any of the given states correlate to the country. | |
2635 | * | |
2636 | * @param int $countryID | |
2637 | * @param array $possibleStates | |
2638 | * | |
2639 | * @return int|null | |
2640 | * @throws \API_Exception | |
2641 | */ | |
2642 | private function checkStatesForCountry(int $countryID, array $possibleStates) { | |
2643 | foreach ($possibleStates as $index => $state) { | |
2644 | if (!empty($this->statesByCountry[$state])) { | |
2645 | if ($this->statesByCountry[$state] === $countryID) { | |
2646 | return $state; | |
2647 | } | |
2648 | unset($possibleStates[$index]); | |
2649 | } | |
2650 | } | |
2651 | if (!empty($possibleStates)) { | |
2652 | $states = StateProvince::get(FALSE) | |
2653 | ->addSelect('country_id') | |
2654 | ->addWhere('id', 'IN', $possibleStates) | |
2655 | ->execute() | |
2656 | ->indexBy('country_id'); | |
2657 | foreach ($states as $state) { | |
2658 | $this->statesByCountry[$state['id']] = $state['country_id']; | |
2659 | } | |
2660 | foreach ($possibleStates as $state) { | |
2661 | if ($this->statesByCountry[$state] === $countryID) { | |
2662 | return $state; | |
2663 | } | |
2664 | } | |
2665 | } | |
2666 | return FALSE; | |
2667 | } | |
2668 | ||
6a488035 | 2669 | } |