(NFC) (dev/core#878) Simplify copyright header (templates/*)
[civicrm-core.git] / CRM / Contact / Form / Contact.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
f299f7db 6 | Copyright CiviCRM LLC (c) 2004-2020 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
ca5cec67 31 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
32 */
33
34/**
35 * This class generates form components generic to all the contact types.
36 *
37 * It delegates the work to lower level subclasses and integrates the changes
38 * back in. It also uses a lot of functionality with the CRM API's, so any change
39 * made here could potentially affect the API etc. Be careful, be aware, use unit tests.
40 *
41 */
42class CRM_Contact_Form_Contact extends CRM_Core_Form {
43
44 /**
fe482240 45 * The contact type of the form.
6a488035
TO
46 *
47 * @var string
48 */
49 public $_contactType;
50
51 /**
fe482240 52 * The contact type of the form.
6a488035
TO
53 *
54 * @var string
55 */
56 public $_contactSubType;
57
58 /**
59 * The contact id, used when editing the form
60 *
61 * @var int
62 */
63 public $_contactId;
64
65 /**
fe482240 66 * The default group id passed in via the url.
6a488035
TO
67 *
68 * @var int
69 */
70 public $_gid;
71
72 /**
fe482240 73 * The default tag id passed in via the url.
6a488035
TO
74 *
75 * @var int
76 */
77 public $_tid;
78
79 /**
100fef9d 80 * Name of de-dupe button
6a488035
TO
81 *
82 * @var string
6a488035
TO
83 */
84 protected $_dedupeButtonName;
85
86 /**
fe482240 87 * Name of optional save duplicate button.
6a488035
TO
88 *
89 * @var string
6a488035
TO
90 */
91 protected $_duplicateButtonName;
92
be2fb01f 93 protected $_editOptions = [];
6a488035 94
be2fb01f 95 protected $_oldSubtypes = [];
6a488035
TO
96
97 public $_blocks;
98
be2fb01f 99 public $_values = [];
6a488035
TO
100
101 public $_action;
102
103 public $_customValueCount;
104 /**
fe482240 105 * The array of greetings with option group and filed names.
6a488035
TO
106 *
107 * @var array
108 */
109 public $_greetings;
110
111 /**
112 * Do we want to parse street address.
69078420 113 * @var bool
6a488035
TO
114 */
115 public $_parseStreetAddress;
116
117 /**
fe482240 118 * Check contact has a subtype or not.
69078420 119 * @var bool
6a488035
TO
120 */
121 public $_isContactSubType;
122
123 /**
124 * Lets keep a cache of all the values that we retrieved.
125 * THis is an attempt to avoid the number of update statements
126 * during the write phase
69078420 127 * @var array
6a488035
TO
128 */
129 public $_preEditValues;
d5965a37 130
6e62b28c
TM
131 /**
132 * Explicitly declare the entity api name.
133 */
134 public function getDefaultEntity() {
135 return 'Contact';
136 }
6a488035 137
1ae720b3
TM
138 /**
139 * Explicitly declare the form context.
140 */
141 public function getDefaultContext() {
142 return 'create';
143 }
144
6a488035 145 /**
fe482240 146 * Build all the data structures needed to build the form.
6a488035 147 */
00be9182 148 public function preProcess() {
6a488035
TO
149 $this->_action = CRM_Utils_Request::retrieve('action', 'String', $this, FALSE, 'add');
150
151 $this->_dedupeButtonName = $this->getButtonName('refresh', 'dedupe');
152 $this->_duplicateButtonName = $this->getButtonName('upload', 'duplicate');
153
0e44568b
CW
154 CRM_Core_Resources::singleton()
155 ->addStyleFile('civicrm', 'css/contactSummary.css', 2, 'html-header');
156
6a488035
TO
157 $session = CRM_Core_Session::singleton();
158 if ($this->_action == CRM_Core_Action::ADD) {
159 // check for add contacts permissions
160 if (!CRM_Core_Permission::check('add contacts')) {
161 CRM_Utils_System::permissionDenied();
162 CRM_Utils_System::civiExit();
163 }
164 $this->_contactType = CRM_Utils_Request::retrieve('ct', 'String',
165 $this, TRUE, NULL, 'REQUEST'
166 );
167 if (!in_array($this->_contactType,
be2fb01f 168 ['Individual', 'Household', 'Organization']
353ffa53
TO
169 )
170 ) {
6a488035
TO
171 CRM_Core_Error::statusBounce(ts('Could not get a contact id and/or contact type'));
172 }
173
174 $this->_isContactSubType = FALSE;
175 if ($this->_contactSubType = CRM_Utils_Request::retrieve('cst', 'String', $this)) {
176 $this->_isContactSubType = TRUE;
177 }
178
179 if (
180 $this->_contactSubType &&
353ffa53
TO
181 !(CRM_Contact_BAO_ContactType::isExtendsContactType($this->_contactSubType, $this->_contactType, TRUE))
182 ) {
be2fb01f 183 CRM_Core_Error::statusBounce(ts("Could not get a valid contact subtype for contact type '%1'", [1 => $this->_contactType]));
6a488035
TO
184 }
185
186 $this->_gid = CRM_Utils_Request::retrieve('gid', 'Integer',
187 CRM_Core_DAO::$_nullObject,
188 FALSE, NULL, 'GET'
189 );
190 $this->_tid = CRM_Utils_Request::retrieve('tid', 'Integer',
191 CRM_Core_DAO::$_nullObject,
192 FALSE, NULL, 'GET'
193 );
194 $typeLabel = CRM_Contact_BAO_ContactType::contactTypePairs(TRUE, $this->_contactSubType ?
353ffa53 195 $this->_contactSubType : $this->_contactType
6a488035
TO
196 );
197 $typeLabel = implode(' / ', $typeLabel);
198
be2fb01f 199 CRM_Utils_System::setTitle(ts('New %1', [1 => $typeLabel]));
6a488035
TO
200 $session->pushUserContext(CRM_Utils_System::url('civicrm/dashboard', 'reset=1'));
201 $this->_contactId = NULL;
202 }
203 else {
204 //update mode
205 if (!$this->_contactId) {
206 $this->_contactId = CRM_Utils_Request::retrieve('cid', 'Positive', $this, TRUE);
207 }
208
209 if ($this->_contactId) {
be2fb01f
CW
210 $defaults = [];
211 $params = ['id' => $this->_contactId];
212 $returnProperities = ['id', 'contact_type', 'contact_sub_type', 'modified_date', 'is_deceased'];
6a488035
TO
213 CRM_Core_DAO::commonRetrieve('CRM_Contact_DAO_Contact', $params, $defaults, $returnProperities);
214
a7488080 215 if (empty($defaults['id'])) {
be2fb01f 216 CRM_Core_Error::statusBounce(ts('A Contact with that ID does not exist: %1', [1 => $this->_contactId]));
6a488035
TO
217 }
218
219 $this->_contactType = CRM_Utils_Array::value('contact_type', $defaults);
220 $this->_contactSubType = CRM_Utils_Array::value('contact_sub_type', $defaults);
221
222 // check for permissions
223 $session = CRM_Core_Session::singleton();
ad623fd4 224 if (!CRM_Contact_BAO_Contact_Permission::allow($this->_contactId, CRM_Core_Permission::EDIT)) {
6a488035
TO
225 CRM_Core_Error::statusBounce(ts('You do not have the necessary permission to edit this contact.'));
226 }
227
228 $displayName = CRM_Contact_BAO_Contact::displayName($this->_contactId);
7d3ddae6 229 if ($defaults['is_deceased']) {
7c38bbf0 230 $displayName .= ' <span class="crm-contact-deceased">(' . ts('deceased') . ')</span>';
7d3ddae6 231 }
be2fb01f 232 $displayName = ts('Edit %1', [1 => $displayName]);
8ef12e64 233
6a488035
TO
234 // Check if this is default domain contact CRM-10482
235 if (CRM_Contact_BAO_Contact::checkDomainContact($this->_contactId)) {
236 $displayName .= ' (' . ts('default organization') . ')';
237 }
8ef12e64 238
6a488035
TO
239 // omitting contactImage from title for now since the summary overlay css doesn't work outside of our crm-container
240 CRM_Utils_System::setTitle($displayName);
edc80cda 241 $context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this);
6a488035
TO
242 $qfKey = CRM_Utils_Request::retrieve('key', 'String', $this);
243
244 $urlParams = 'reset=1&cid=' . $this->_contactId;
245 if ($context) {
246 $urlParams .= "&context=$context";
247 }
248
249 if (CRM_Utils_Rule::qfKey($qfKey)) {
250
251 $urlParams .= "&key=$qfKey";
252
253 }
254 $session->pushUserContext(CRM_Utils_System::url('civicrm/contact/view', $urlParams));
255
256 $values = $this->get('values');
257 // get contact values.
258 if (!empty($values)) {
259 $this->_values = $values;
260 }
261 else {
be2fb01f 262 $params = [
6a488035
TO
263 'id' => $this->_contactId,
264 'contact_id' => $this->_contactId,
265 'noRelationships' => TRUE,
266 'noNotes' => TRUE,
267 'noGroups' => TRUE,
be2fb01f 268 ];
6a488035
TO
269
270 $contact = CRM_Contact_BAO_Contact::retrieve($params, $this->_values, TRUE);
271 $this->set('values', $this->_values);
272 }
273 }
274 else {
275 CRM_Core_Error::statusBounce(ts('Could not get a contact_id and/or contact_type'));
276 }
277 }
278
279 // parse street address, CRM-5450
280 $this->_parseStreetAddress = $this->get('parseStreetAddress');
281 if (!isset($this->_parseStreetAddress)) {
282 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
283 'address_options'
284 );
285 $this->_parseStreetAddress = FALSE;
8cc574cf 286 if (!empty($addressOptions['street_address']) && !empty($addressOptions['street_address_parsing'])) {
6a488035
TO
287 $this->_parseStreetAddress = TRUE;
288 }
289 $this->set('parseStreetAddress', $this->_parseStreetAddress);
290 }
291 $this->assign('parseStreetAddress', $this->_parseStreetAddress);
292
293 $this->_editOptions = $this->get('contactEditOptions');
294 if (CRM_Utils_System::isNull($this->_editOptions)) {
295 $this->_editOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
296 'contact_edit_options', TRUE, NULL,
297 FALSE, 'name', TRUE, 'AND v.filter = 0'
298 );
299 $this->set('contactEditOptions', $this->_editOptions);
300 }
301
302 // build demographics only for Individual contact type
303 if ($this->_contactType != 'Individual' &&
304 array_key_exists('Demographics', $this->_editOptions)
305 ) {
306 unset($this->_editOptions['Demographics']);
307 }
308
309 // in update mode don't show notes
310 if ($this->_contactId && array_key_exists('Notes', $this->_editOptions)) {
311 unset($this->_editOptions['Notes']);
312 }
313
314 $this->assign('editOptions', $this->_editOptions);
315 $this->assign('contactType', $this->_contactType);
316 $this->assign('contactSubType', $this->_contactSubType);
317
6a488035
TO
318 // get the location blocks.
319 $this->_blocks = $this->get('blocks');
320 if (CRM_Utils_System::isNull($this->_blocks)) {
321 $this->_blocks = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
322 'contact_edit_options', TRUE, NULL,
323 FALSE, 'name', TRUE, 'AND v.filter = 1'
324 );
325 $this->set('blocks', $this->_blocks);
326 }
327 $this->assign('blocks', $this->_blocks);
328
329 // this is needed for custom data.
330 $this->assign('entityID', $this->_contactId);
331
332 // also keep the convention.
333 $this->assign('contactId', $this->_contactId);
334
335 // location blocks.
336 CRM_Contact_Form_Location::preProcess($this);
337
338 // retain the multiple count custom fields value
a7488080 339 if (!empty($_POST['hidden_custom'])) {
6a488035
TO
340 $customGroupCount = CRM_Utils_Array::value('hidden_custom_group_count', $_POST);
341
481a74f4 342 if ($contactSubType = CRM_Utils_Array::value('contact_sub_type', $_POST)) {
6a488035
TO
343 $paramSubType = implode(',', $contactSubType);
344 }
345
346 $this->_getCachedTree = FALSE;
347 unset($customGroupCount[0]);
348 foreach ($customGroupCount as $groupID => $groupCount) {
349 if ($groupCount > 1) {
350 $this->set('groupID', $groupID);
351 //loop the group
352 for ($i = 0; $i <= $groupCount; $i++) {
353 CRM_Custom_Form_CustomData::preProcess($this, NULL, $contactSubType,
e6a1bdbe 354 $i, $this->_contactType, $this->_contactId
6a488035
TO
355 );
356 CRM_Contact_Form_Edit_CustomData::buildQuickForm($this);
357 }
358 }
359 }
360
361 //reset all the ajax stuff, for normal processing
362 if (isset($this->_groupTree)) {
363 $this->_groupTree = NULL;
364 }
365 $this->set('groupID', NULL);
366 $this->_getCachedTree = TRUE;
367 }
368
369 // execute preProcess dynamically by js else execute normal preProcess
370 if (array_key_exists('CustomData', $this->_editOptions)) {
371 //assign a parameter to pass for sub type multivalue
372 //custom field to load
373 if ($this->_contactSubType || isset($paramSubType)) {
374 $paramSubType = (isset($paramSubType)) ? $paramSubType :
481a74f4 375 str_replace(CRM_Core_DAO::VALUE_SEPARATOR, ',', trim($this->_contactSubType, CRM_Core_DAO::VALUE_SEPARATOR));
6a488035
TO
376
377 $this->assign('paramSubType', $paramSubType);
378 }
379
a3d827a7 380 if (CRM_Utils_Request::retrieve('type', 'String')) {
6a488035
TO
381 CRM_Contact_Form_Edit_CustomData::preProcess($this);
382 }
383 else {
384 $contactSubType = $this->_contactSubType;
385 // need contact sub type to build related grouptree array during post process
a7488080 386 if (!empty($_POST['contact_sub_type'])) {
6a488035
TO
387 $contactSubType = $_POST['contact_sub_type'];
388 }
389 //only custom data has preprocess hence directly call it
390 CRM_Custom_Form_CustomData::preProcess($this, NULL, $contactSubType,
391 1, $this->_contactType, $this->_contactId
392 );
393 $this->assign('customValueCount', $this->_customValueCount);
394 }
395 }
396 }
397
398 /**
c037736a 399 * Set default values for the form.
6a488035 400 *
c037736a 401 * Note that in edit/view mode the default values are retrieved from the database
6a488035 402 */
00be9182 403 public function setDefaultValues() {
6a488035 404 $defaults = $this->_values;
6a488035
TO
405
406 if ($this->_action & CRM_Core_Action::ADD) {
407 if (array_key_exists('TagsAndGroups', $this->_editOptions)) {
408 // set group and tag defaults if any
409 if ($this->_gid) {
c18f95b7 410 $defaults['group'][] = $this->_gid;
6a488035
TO
411 }
412 if ($this->_tid) {
413 $defaults['tag'][$this->_tid] = 1;
414 }
415 }
416 if ($this->_contactSubType) {
417 $defaults['contact_sub_type'] = $this->_contactSubType;
418 }
419 }
420 else {
6a488035
TO
421 foreach ($defaults['email'] as $dontCare => & $val) {
422 if (isset($val['signature_text'])) {
423 $val['signature_text_hidden'] = $val['signature_text'];
424 }
425 if (isset($val['signature_html'])) {
426 $val['signature_html_hidden'] = $val['signature_html'];
427 }
428 }
429
a7488080 430 if (!empty($defaults['contact_sub_type'])) {
6a488035
TO
431 $defaults['contact_sub_type'] = $this->_oldSubtypes;
432 }
433 }
6a488035
TO
434 // set defaults for blocks ( custom data, address, communication preference, notes, tags and groups )
435 foreach ($this->_editOptions as $name => $label) {
be2fb01f 436 if (!in_array($name, ['Address', 'Notes'])) {
150f50c1
CW
437 $className = 'CRM_Contact_Form_Edit_' . $name;
438 $className::setDefaultValues($this, $defaults);
6a488035
TO
439 }
440 }
441
442 //set address block defaults
481a74f4 443 CRM_Contact_Form_Edit_Address::setDefaultValues($defaults, $this);
6a488035 444
a7488080 445 if (!empty($defaults['image_URL'])) {
c3821398 446 $this->assign("imageURL", CRM_Utils_File::getImageURL($defaults['image_URL']));
6a488035
TO
447 }
448
449 //set location type and country to default for each block
450 $this->blockSetDefaults($defaults);
451
452 $this->_preEditValues = $defaults;
453 return $defaults;
454 }
455
456 /**
ea3ddccf 457 * Do the set default related to location type id, primary location, default country.
458 *
459 * @param array $defaults
6a488035 460 */
00be9182 461 public function blockSetDefaults(&$defaults) {
b2b0530a 462 $locationTypeKeys = array_filter(array_keys(CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id')), 'is_int');
6a488035
TO
463 sort($locationTypeKeys);
464
465 // get the default location type
466 $locationType = CRM_Core_BAO_LocationType::getDefault();
467
468 // unset primary location type
469 $primaryLocationTypeIdKey = CRM_Utils_Array::key($locationType->id, $locationTypeKeys);
470 unset($locationTypeKeys[$primaryLocationTypeIdKey]);
471
472 // reset the array sequence
473 $locationTypeKeys = array_values($locationTypeKeys);
474
475 // get default phone and im provider id.
476 $defPhoneTypeId = key(CRM_Core_OptionGroup::values('phone_type', FALSE, FALSE, FALSE, ' AND is_default = 1'));
477 $defIMProviderId = key(CRM_Core_OptionGroup::values('instant_messenger_service',
353ffa53
TO
478 FALSE, FALSE, FALSE, ' AND is_default = 1'
479 ));
1d477756
PN
480 $defWebsiteTypeId = key(CRM_Core_OptionGroup::values('website_type',
481 FALSE, FALSE, FALSE, ' AND is_default = 1'
482 ));
6a488035
TO
483
484 $allBlocks = $this->_blocks;
485 if (array_key_exists('Address', $this->_editOptions)) {
486 $allBlocks['Address'] = $this->_editOptions['Address'];
487 }
488
489 $config = CRM_Core_Config::singleton();
490 foreach ($allBlocks as $blockName => $label) {
491 $name = strtolower($blockName);
492 $hasPrimary = $updateMode = FALSE;
493
494 // user is in update mode.
495 if (array_key_exists($name, $defaults) &&
496 !CRM_Utils_System::isNull($defaults[$name])
497 ) {
498 $updateMode = TRUE;
499 }
500
501 for ($instance = 1; $instance <= $this->get($blockName . '_Block_Count'); $instance++) {
502 // make we require one primary block, CRM-5505
594833d6 503 if ($updateMode) {
23839387 504 if (!$hasPrimary) {
594833d6 505 $hasPrimary = CRM_Utils_Array::value(
506 'is_primary',
507 CRM_Utils_Array::value($instance, $defaults[$name])
508 );
509 }
6a488035
TO
510 continue;
511 }
512
513 //set location to primary for first one.
514 if ($instance == 1) {
515 $hasPrimary = TRUE;
516 $defaults[$name][$instance]['is_primary'] = TRUE;
517 $defaults[$name][$instance]['location_type_id'] = $locationType->id;
518 }
519 else {
520 $locTypeId = isset($locationTypeKeys[$instance - 1]) ? $locationTypeKeys[$instance - 1] : $locationType->id;
521 $defaults[$name][$instance]['location_type_id'] = $locTypeId;
522 }
523
524 //set default country
525 if ($name == 'address' && $config->defaultContactCountry) {
526 $defaults[$name][$instance]['country_id'] = $config->defaultContactCountry;
527 }
528
529 //set default state/province
530 if ($name == 'address' && $config->defaultContactStateProvince) {
531 $defaults[$name][$instance]['state_province_id'] = $config->defaultContactStateProvince;
532 }
533
534 //set default phone type.
535 if ($name == 'phone' && $defPhoneTypeId) {
536 $defaults[$name][$instance]['phone_type_id'] = $defPhoneTypeId;
537 }
1d477756
PN
538 //set default website type.
539 if ($name == 'website' && $defWebsiteTypeId) {
540 $defaults[$name][$instance]['website_type_id'] = $defWebsiteTypeId;
541 }
6a488035
TO
542
543 //set default im provider.
544 if ($name == 'im' && $defIMProviderId) {
545 $defaults[$name][$instance]['provider_id'] = $defIMProviderId;
546 }
547 }
548
549 if (!$hasPrimary) {
550 $defaults[$name][1]['is_primary'] = TRUE;
551 }
552 }
6a488035
TO
553 }
554
555 /**
dc195289 556 * add the rules (mainly global rules) for form.
6a488035
TO
557 * All local rules are added near the element
558 *
6a488035
TO
559 * @see valid_date
560 */
00be9182 561 public function addRules() {
6a488035
TO
562 // skip adding formRules when custom data is build
563 if ($this->_addBlockName || ($this->_action & CRM_Core_Action::DELETE)) {
564 return;
565 }
566
be2fb01f 567 $this->addFormRule(['CRM_Contact_Form_Edit_' . $this->_contactType, 'formRule'], $this->_contactId);
6a488035
TO
568
569 // Call Locking check if editing existing contact
570 if ($this->_contactId) {
be2fb01f 571 $this->addFormRule(['CRM_Contact_Form_Edit_Lock', 'formRule'], $this->_contactId);
6a488035
TO
572 }
573
574 if (array_key_exists('Address', $this->_editOptions)) {
be2fb01f 575 $this->addFormRule(['CRM_Contact_Form_Edit_Address', 'formRule'], $this);
6a488035
TO
576 }
577
578 if (array_key_exists('CommunicationPreferences', $this->_editOptions)) {
be2fb01f 579 $this->addFormRule(['CRM_Contact_Form_Edit_CommunicationPreferences', 'formRule'], $this);
6a488035
TO
580 }
581 }
582
583 /**
fe482240 584 * Global validation rules for the form.
6a488035 585 *
77c5b619
TO
586 * @param array $fields
587 * Posted values of the form.
588 * @param array $errors
589 * List of errors to be posted back to the form.
590 * @param int $contactId
591 * Contact id if doing update.
69078420 592 * @param string $contactType
6a488035 593 *
a6c01b45
CW
594 * @return bool
595 * email/openId
6a488035 596 */
d6def514 597 public static function formRule($fields, &$errors, $contactId, $contactType) {
6a488035
TO
598 $config = CRM_Core_Config::singleton();
599
600 // validations.
601 //1. for each block only single value can be marked as is_primary = true.
602 //2. location type id should be present if block data present.
603 //3. check open id across db and other each block for duplicate.
604 //4. at least one location should be primary.
605 //5. also get primaryID from email or open id block.
606
607 // take the location blocks.
608 $blocks = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
609 'contact_edit_options', TRUE, NULL,
610 FALSE, 'name', TRUE, 'AND v.filter = 1'
611 );
612
613 $otherEditOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
614 'contact_edit_options', TRUE, NULL,
615 FALSE, 'name', TRUE, 'AND v.filter = 0'
616 );
617 //get address block inside.
618 if (array_key_exists('Address', $otherEditOptions)) {
619 $blocks['Address'] = $otherEditOptions['Address'];
620 }
621
be2fb01f
CW
622 $website_types = [];
623 $openIds = [];
6a488035
TO
624 $primaryID = FALSE;
625 foreach ($blocks as $name => $label) {
be2fb01f 626 $hasData = $hasPrimary = [];
6a488035 627 $name = strtolower($name);
a7488080 628 if (!empty($fields[$name]) && is_array($fields[$name])) {
6a488035
TO
629 foreach ($fields[$name] as $instance => $blockValues) {
630 $dataExists = self::blockDataExists($blockValues);
631
632 if (!$dataExists && $name == 'address') {
633 $dataExists = CRM_Utils_Array::value('use_shared_address', $fields['address'][$instance]);
634 }
635
636 if ($dataExists) {
6a488035 637 if ($name == 'website') {
778fd763
GB
638 if (!empty($blockValues['website_type_id'])) {
639 if (empty($website_types[$blockValues['website_type_id']])) {
640 $website_types[$blockValues['website_type_id']] = $blockValues['website_type_id'];
641 }
642 else {
643 $errors["{$name}[1][website_type_id]"] = ts('Contacts may only have one website of each type at most.');
644 }
645 }
646
647 // skip remaining checks for website
6a488035
TO
648 continue;
649 }
650
651 $hasData[] = $instance;
a7488080 652 if (!empty($blockValues['is_primary'])) {
6a488035
TO
653 $hasPrimary[] = $instance;
654 if (!$primaryID &&
be2fb01f 655 in_array($name, [
353ffa53 656 'email',
af9b09df 657 'openid',
be2fb01f 658 ]) && !empty($blockValues[$name])
353ffa53 659 ) {
6a488035
TO
660 $primaryID = $blockValues[$name];
661 }
662 }
663
a7488080 664 if (empty($blockValues['location_type_id'])) {
be2fb01f 665 $errors["{$name}[$instance][location_type_id]"] = ts('The Location Type should be set if there is %1 information.', [1 => $label]);
6a488035
TO
666 }
667 }
668
8cc574cf 669 if ($name == 'openid' && !empty($blockValues[$name])) {
353ffa53 670 $oid = new CRM_Core_DAO_OpenID();
6a488035 671 $oid->openid = $openIds[$instance] = CRM_Utils_Array::value($name, $blockValues);
353ffa53 672 $cid = isset($contactId) ? $contactId : 0;
6a488035 673 if ($oid->find(TRUE) && ($oid->contact_id != $cid)) {
be2fb01f 674 $errors["{$name}[$instance][openid]"] = ts('%1 already exist.', [1 => $blocks['OpenID']]);
6a488035
TO
675 }
676 }
677 }
678
679 if (empty($hasPrimary) && !empty($hasData)) {
be2fb01f 680 $errors["{$name}[1][is_primary]"] = ts('One %1 should be marked as primary.', [1 => $label]);
6a488035
TO
681 }
682
683 if (count($hasPrimary) > 1) {
684 $errors["{$name}[" . array_pop($hasPrimary) . "][is_primary]"] = ts('Only one %1 can be marked as primary.',
be2fb01f 685 [1 => $label]
6a488035
TO
686 );
687 }
688 }
689 }
690
691 //do validations for all opend ids they should be distinct.
692 if (!empty($openIds) && (count(array_unique($openIds)) != count($openIds))) {
693 foreach ($openIds as $instance => $value) {
694 if (!array_key_exists($instance, array_unique($openIds))) {
be2fb01f 695 $errors["openid[$instance][openid]"] = ts('%1 already used.', [1 => $blocks['OpenID']]);
6a488035
TO
696 }
697 }
698 }
699
700 // street number should be digit + suffix, CRM-5450
701 $parseStreetAddress = CRM_Utils_Array::value('street_address_parsing',
702 CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
703 'address_options'
704 )
705 );
706 if ($parseStreetAddress) {
707 if (isset($fields['address']) &&
708 is_array($fields['address'])
709 ) {
be2fb01f 710 $invalidStreetNumbers = [];
6a488035
TO
711 foreach ($fields['address'] as $cnt => $address) {
712 if ($streetNumber = CRM_Utils_Array::value('street_number', $address)) {
713 $parsedAddress = CRM_Core_BAO_Address::parseStreetAddress($address['street_number']);
a7488080 714 if (empty($parsedAddress['street_number'])) {
6a488035
TO
715 $invalidStreetNumbers[] = $cnt;
716 }
717 }
718 }
719
720 if (!empty($invalidStreetNumbers)) {
721 $first = $invalidStreetNumbers[0];
353ffa53
TO
722 foreach ($invalidStreetNumbers as & $num) {
723 $num = CRM_Contact_Form_Contact::ordinalNumber($num);
724 }
be2fb01f 725 $errors["address[$first][street_number]"] = ts('The street number you entered for the %1 address block(s) is not in an expected format. Street numbers may include numeric digit(s) followed by other characters. You can still enter the complete street address (unparsed) by clicking "Edit Complete Street Address".', [1 => implode(', ', $invalidStreetNumbers)]);
6a488035
TO
726 }
727 }
728 }
729
d6def514 730 // Check for duplicate contact if it wasn't already handled by ajax or disabled
a7ba493c 731 if (!Civi::settings()->get('contact_ajax_check_similar') || !empty($fields['_qf_Contact_refresh_dedupe'])) {
d6def514
CW
732 self::checkDuplicateContacts($fields, $errors, $contactId, $contactType);
733 }
734
6a488035
TO
735 return $primaryID;
736 }
737
738 /**
fe482240 739 * Build the form object.
6a488035
TO
740 */
741 public function buildQuickForm() {
742 //load form for child blocks
743 if ($this->_addBlockName) {
150f50c1
CW
744 $className = 'CRM_Contact_Form_Edit_' . $this->_addBlockName;
745 return $className::buildQuickForm($this);
6a488035
TO
746 }
747
748 if ($this->_action == CRM_Core_Action::UPDATE) {
e8fb9449 749 $deleteExtra = json_encode(ts('Are you sure you want to delete contact image.'));
be2fb01f
CW
750 $deleteURL = [
751 CRM_Core_Action::DELETE => [
af9b09df
TO
752 'name' => ts('Delete Contact Image'),
753 'url' => 'civicrm/contact/image',
754 'qs' => 'reset=1&cid=%%id%%&action=delete',
e8fb9449 755 'extra' => 'onclick = "' . htmlspecialchars("if (confirm($deleteExtra)) this.href+='&confirmed=1'; else return false;") . '"',
be2fb01f
CW
756 ],
757 ];
6a488035
TO
758 $deleteURL = CRM_Core_Action::formLink($deleteURL,
759 CRM_Core_Action::DELETE,
be2fb01f 760 [
6a488035 761 'id' => $this->_contactId,
be2fb01f 762 ],
87dab4a4
AH
763 ts('more'),
764 FALSE,
765 'contact.image.delete',
766 'Contact',
767 $this->_contactId
6a488035
TO
768 );
769 $this->assign('deleteURL', $deleteURL);
770 }
771
772 //build contact type specific fields
150f50c1
CW
773 $className = 'CRM_Contact_Form_Edit_' . $this->_contactType;
774 $className::buildQuickForm($this);
6a488035 775
d6def514 776 // Ajax duplicate checking
a7ba493c 777 $checkSimilar = Civi::settings()->get('contact_ajax_check_similar');
d6def514
CW
778 $this->assign('checkSimilar', $checkSimilar);
779 if ($checkSimilar == 1) {
be2fb01f 780 $ruleParams = ['used' => 'Supervised', 'contact_type' => $this->_contactType];
d6def514
CW
781 $this->assign('ruleFields', CRM_Dedupe_BAO_Rule::dedupeRuleFields($ruleParams));
782 }
783
6a488035
TO
784 // build Custom data if Custom data present in edit option
785 $buildCustomData = 'noCustomDataPresent';
786 if (array_key_exists('CustomData', $this->_editOptions)) {
787 $buildCustomData = "customDataPresent";
788 }
789
790 // subtype is a common field. lets keep it here
be2fb01f 791 $subtypes = CRM_Contact_BAO_Contact::buildOptions('contact_sub_type', 'create', ['contact_type' => $this->_contactType]);
6a488035 792 if (!empty($subtypes)) {
be2fb01f 793 $this->addField('contact_sub_type', [
33fa033c
TM
794 'label' => ts('Contact Type'),
795 'options' => $subtypes,
796 'class' => $buildCustomData,
797 'multiple' => 'multiple',
599ae208 798 'option_url' => NULL,
be2fb01f 799 ]);
6a488035
TO
800 }
801
802 // build edit blocks ( custom data, demographics, communication preference, notes, tags and groups )
803 foreach ($this->_editOptions as $name => $label) {
804 if ($name == 'Address') {
805 $this->_blocks['Address'] = $this->_editOptions['Address'];
806 continue;
807 }
c18f95b7
PJ
808 if ($name == 'TagsAndGroups') {
809 continue;
810 }
150f50c1
CW
811 $className = 'CRM_Contact_Form_Edit_' . $name;
812 $className::buildQuickForm($this);
6a488035
TO
813 }
814
c18f95b7
PJ
815 // build tags and groups
816 CRM_Contact_Form_Edit_TagsAndGroups::buildQuickForm($this, 0, CRM_Contact_Form_Edit_TagsAndGroups::ALL,
ab345ca5 817 FALSE, NULL, 'Group(s)', 'Tag(s)', NULL, 'select');
c18f95b7 818
6a488035
TO
819 // build location blocks.
820 CRM_Contact_Form_Edit_Lock::buildQuickForm($this);
821 CRM_Contact_Form_Location::buildQuickForm($this);
822
823 // add attachment
be2fb01f 824 $this->addField('image_URL', ['maxlength' => '255', 'label' => ts('Browse/Upload Image')]);
6a488035
TO
825
826 // add the dedupe button
827 $this->addElement('submit',
828 $this->_dedupeButtonName,
829 ts('Check for Matching Contact(s)')
830 );
831 $this->addElement('submit',
832 $this->_duplicateButtonName,
833 ts('Save Matching Contact')
834 );
835 $this->addElement('submit',
836 $this->getButtonName('next', 'sharedHouseholdDuplicate'),
837 ts('Save With Duplicate Household')
838 );
839
be2fb01f
CW
840 $buttons = [
841 [
6a488035
TO
842 'type' => 'upload',
843 'name' => ts('Save'),
844 'subName' => 'view',
845 'isDefault' => TRUE,
be2fb01f
CW
846 ],
847 ];
1870cae9 848 if (CRM_Core_Permission::check('add contacts')) {
be2fb01f 849 $buttons[] = [
6a488035
TO
850 'type' => 'upload',
851 'name' => ts('Save and New'),
852 'spacing' => '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
853 'subName' => 'new',
be2fb01f 854 ];
1870cae9 855 }
be2fb01f 856 $buttons[] = [
1870cae9
CW
857 'type' => 'cancel',
858 'name' => ts('Cancel'),
be2fb01f 859 ];
6a488035 860
a7488080 861 if (!empty($this->_values['contact_sub_type'])) {
6a488035
TO
862 $this->_oldSubtypes = explode(CRM_Core_DAO::VALUE_SEPARATOR,
863 trim($this->_values['contact_sub_type'], CRM_Core_DAO::VALUE_SEPARATOR)
864 );
865 }
866 $this->assign('oldSubtypes', json_encode($this->_oldSubtypes));
867
868 $this->addButtons($buttons);
869 }
870
871 /**
872 * Form submission of new/edit contact is processed.
6a488035
TO
873 */
874 public function postProcess() {
875 // check if dedupe button, if so return.
876 $buttonName = $this->controller->getButtonName();
877 if ($buttonName == $this->_dedupeButtonName) {
878 return;
879 }
880
881 //get the submitted values in an array
882 $params = $this->controller->exportValues($this->_name);
37f7ae88 883 if (!isset($params['preferred_communication_method'])) {
884 // If this field is empty QF will trim it so we have to add it in.
885 $params['preferred_communication_method'] = 'null';
886 }
6a488035 887
c18f95b7 888 $group = CRM_Utils_Array::value('group', $params);
7e6c32e8 889 if (!empty($group) && is_array($group)) {
c18f95b7
PJ
890 unset($params['group']);
891 foreach ($group as $key => $value) {
892 $params['group'][$value] = 1;
893 }
894 }
895
a7488080 896 if (!empty($params['image_URL'])) {
6a488035
TO
897 CRM_Contact_BAO_Contact::processImageParams($params);
898 }
899
8cc574cf 900 if (is_numeric(CRM_Utils_Array::value('current_employer_id', $params)) && !empty($params['current_employer'])) {
6a488035
TO
901 $params['current_employer'] = $params['current_employer_id'];
902 }
903
904 // don't carry current_employer_id field,
905 // since we don't want to directly update DAO object without
906 // handling related business logic ( eg related membership )
907 if (isset($params['current_employer_id'])) {
908 unset($params['current_employer_id']);
909 }
910
911 $params['contact_type'] = $this->_contactType;
912 if (empty($params['contact_sub_type']) && $this->_isContactSubType) {
be2fb01f 913 $params['contact_sub_type'] = [$this->_contactSubType];
6a488035
TO
914 }
915
916 if ($this->_contactId) {
917 $params['contact_id'] = $this->_contactId;
918 }
919
920 //make deceased date null when is_deceased = false
8cc574cf 921 if ($this->_contactType == 'Individual' && !empty($this->_editOptions['Demographics']) && empty($params['is_deceased'])) {
6a488035
TO
922 $params['is_deceased'] = FALSE;
923 $params['deceased_date'] = NULL;
924 }
925
926 if (isset($params['contact_id'])) {
927 // process membership status for deceased contact
be2fb01f 928 $deceasedParams = [
6ea503d4 929 'contact_id' => CRM_Utils_Array::value('contact_id', $params),
6a488035
TO
930 'is_deceased' => CRM_Utils_Array::value('is_deceased', $params, FALSE),
931 'deceased_date' => CRM_Utils_Array::value('deceased_date', $params, NULL),
be2fb01f 932 ];
6a488035
TO
933 $updateMembershipMsg = $this->updateMembershipStatus($deceasedParams);
934 }
935
936 // action is taken depending upon the mode
937 if ($this->_action & CRM_Core_Action::UPDATE) {
938 CRM_Utils_Hook::pre('edit', $params['contact_type'], $params['contact_id'], $params);
939 }
940 else {
941 CRM_Utils_Hook::pre('create', $params['contact_type'], NULL, $params);
942 }
943
6a488035
TO
944 //CRM-5143
945 //if subtype is set, send subtype as extend to validate subtype customfield
946 $customFieldExtends = (CRM_Utils_Array::value('contact_sub_type', $params)) ? $params['contact_sub_type'] : $params['contact_type'];
947
948 $params['custom'] = CRM_Core_BAO_CustomField::postProcess($params,
6a488035
TO
949 $this->_contactId,
950 $customFieldExtends,
951 TRUE
952 );
953 if ($this->_contactId && !empty($this->_oldSubtypes)) {
954 CRM_Contact_BAO_ContactType::deleteCustomSetForSubtypeMigration($this->_contactId,
955 $params['contact_type'],
956 $this->_oldSubtypes,
957 $params['contact_sub_type']
958 );
959 }
960
961 if (array_key_exists('CommunicationPreferences', $this->_editOptions)) {
962 // this is a chekbox, so mark false if we dont get a POST value
963 $params['is_opt_out'] = CRM_Utils_Array::value('is_opt_out', $params, FALSE);
964 }
965
966 // process shared contact address.
967 CRM_Contact_BAO_Contact_Utils::processSharedAddress($params['address']);
968
0808ea6a 969 if (!array_key_exists('TagsAndGroups', $this->_editOptions)) {
6a488035
TO
970 unset($params['group']);
971 }
0808ea6a 972 elseif (!empty($params['contact_id']) && ($this->_action & CRM_Core_Action::UPDATE)) {
6a488035 973 // figure out which all groups are intended to be removed
c18f95b7
PJ
974 $contactGroupList = CRM_Contact_BAO_GroupContact::getContactGroup($params['contact_id'], 'Added');
975 if (is_array($contactGroupList)) {
976 foreach ($contactGroupList as $key) {
8cc574cf 977 if ((!array_key_exists($key['group_id'], $params['group']) || $params['group'][$key['group_id']] != 1) && empty($key['is_hidden'])) {
c18f95b7 978 $params['group'][$key['group_id']] = -1;
6a488035
TO
979 }
980 }
981 }
982 }
983
984 // parse street address, CRM-5450
985 $parseStatusMsg = NULL;
986 if ($this->_parseStreetAddress) {
987 $parseResult = self::parseAddress($params);
988 $parseStatusMsg = self::parseAddressStatusMsg($parseResult);
989 }
990
be2fb01f 991 $blocks = ['email', 'phone', 'im', 'openid', 'address', 'website'];
85c882c5 992 foreach ($blocks as $block) {
a3b489b9 993 if (!empty($this->_preEditValues[$block]) && is_array($this->_preEditValues[$block])) {
994 foreach ($this->_preEditValues[$block] as $count => $value) {
995 if (!empty($value['id'])) {
996 $params[$block][$count]['id'] = $value['id'];
997 $params[$block]['isIdSet'] = TRUE;
998 }
85c882c5 999 }
1000 }
1001 }
1002
6a488035
TO
1003 // Allow un-setting of location info, CRM-5969
1004 $params['updateBlankLocInfo'] = TRUE;
1005
1006 $contact = CRM_Contact_BAO_Contact::create($params, TRUE, FALSE, TRUE);
1007
1008 // status message
1009 if ($this->_contactId) {
be2fb01f 1010 $message = ts('%1 has been updated.', [1 => $contact->display_name]);
6a488035
TO
1011 }
1012 else {
be2fb01f 1013 $message = ts('%1 has been created.', [1 => $contact->display_name]);
6a488035
TO
1014 }
1015
1016 // set the contact ID
1017 $this->_contactId = $contact->id;
1018
1019 if (array_key_exists('TagsAndGroups', $this->_editOptions)) {
1020 //add contact to tags
85085150 1021 if (isset($params['tag'])) {
d8d2f9e1 1022 $params['tag'] = array_flip(explode(',', $params['tag']));
1023 CRM_Core_BAO_EntityTag::create($params['tag'], 'civicrm_contact', $params['contact_id']);
1024 }
6a488035
TO
1025 //save free tags
1026 if (isset($params['contact_taglist']) && !empty($params['contact_taglist'])) {
1027 CRM_Core_Form_Tag::postProcess($params['contact_taglist'], $params['contact_id'], 'civicrm_contact', $this);
1028 }
1029 }
1030
1031 if (!empty($parseStatusMsg)) {
1032 $message .= "<br />$parseStatusMsg";
1033 }
1034 if (!empty($updateMembershipMsg)) {
1035 $message .= "<br />$updateMembershipMsg";
1036 }
1037
1038 $session = CRM_Core_Session::singleton();
1039 $session->setStatus($message, ts('Contact Saved'), 'success');
1040
1041 // add the recently viewed contact
be2fb01f 1042 $recentOther = [];
6a488035
TO
1043 if (($session->get('userID') == $contact->id) ||
1044 CRM_Contact_BAO_Contact_Permission::allow($contact->id, CRM_Core_Permission::EDIT)
1045 ) {
1046 $recentOther['editUrl'] = CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contact->id);
1047 }
1048
1049 if (($session->get('userID') != $this->_contactId) && CRM_Core_Permission::check('delete contacts')) {
1050 $recentOther['deleteUrl'] = CRM_Utils_System::url('civicrm/contact/view/delete', 'reset=1&delete=1&cid=' . $contact->id);
1051 }
1052
1053 CRM_Utils_Recent::add($contact->display_name,
1054 CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $contact->id),
1055 $contact->id,
1056 $this->_contactType,
1057 $contact->id,
1058 $contact->display_name,
1059 $recentOther
1060 );
1061
1062 // here we replace the user context with the url to view this contact
1063 $buttonName = $this->controller->getButtonName();
1064 if ($buttonName == $this->getButtonName('upload', 'new')) {
69dde52d 1065 $contactSubTypes = array_filter(explode(CRM_Core_DAO::VALUE_SEPARATOR, $this->_contactSubType));
6a488035 1066 $resetStr = "reset=1&ct={$contact->contact_type}";
69dde52d 1067 $resetStr .= (count($contactSubTypes) == 1) ? "&cst=" . array_pop($contactSubTypes) : '';
6a488035
TO
1068 $session->replaceUserContext(CRM_Utils_System::url('civicrm/contact/add', $resetStr));
1069 }
1070 else {
edc80cda 1071 $context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this);
6a488035
TO
1072 $qfKey = CRM_Utils_Request::retrieve('key', 'String', $this);
1073 //validate the qfKey
1074 $urlParams = 'reset=1&cid=' . $contact->id;
1075 if ($context) {
1076 $urlParams .= "&context=$context";
1077 }
1078 if (CRM_Utils_Rule::qfKey($qfKey)) {
1079 $urlParams .= "&key=$qfKey";
1080 }
1081
1082 $session->replaceUserContext(CRM_Utils_System::url('civicrm/contact/view', $urlParams));
1083 }
1084
1085 // now invoke the post hook
1086 if ($this->_action & CRM_Core_Action::UPDATE) {
1087 CRM_Utils_Hook::post('edit', $params['contact_type'], $contact->id, $contact);
1088 }
1089 else {
1090 CRM_Utils_Hook::post('create', $params['contact_type'], $contact->id, $contact);
1091 }
1092 }
1093
1094 /**
fe482240 1095 * Is there any real significant data in the hierarchical location array.
6a488035 1096 *
77c5b619
TO
1097 * @param array $fields
1098 * The hierarchical value representation of this location.
6a488035 1099 *
5c766a0b 1100 * @return bool
a6c01b45 1101 * true if data exists, false otherwise
6a488035 1102 */
00be9182 1103 public static function blockDataExists(&$fields) {
6a488035
TO
1104 if (!is_array($fields)) {
1105 return FALSE;
1106 }
1107
be2fb01f 1108 static $skipFields = [
353ffa53
TO
1109 'location_type_id',
1110 'is_primary',
1111 'phone_type_id',
1112 'provider_id',
1113 'country_id',
1114 'website_type_id',
af9b09df 1115 'master_id',
be2fb01f 1116 ];
6a488035
TO
1117 foreach ($fields as $name => $value) {
1118 $skipField = FALSE;
1119 foreach ($skipFields as $skip) {
1120 if (strpos("[$skip]", $name) !== FALSE) {
1121 if ($name == 'phone') {
1122 continue;
1123 }
1124 $skipField = TRUE;
1125 break;
1126 }
1127 }
1128 if ($skipField) {
1129 continue;
1130 }
1131 if (is_array($value)) {
1132 if (self::blockDataExists($value)) {
1133 return TRUE;
1134 }
1135 }
1136 else {
1137 if (!empty($value)) {
1138 return TRUE;
1139 }
1140 }
1141 }
1142
1143 return FALSE;
1144 }
1145
1146 /**
fe482240 1147 * That checks for duplicate contacts.
6a488035 1148 *
77c5b619
TO
1149 * @param array $fields
1150 * Fields array which are submitted.
fd31fa4c 1151 * @param $errors
77c5b619
TO
1152 * @param int $contactID
1153 * Contact id.
1154 * @param string $contactType
1155 * Contact type.
6a488035 1156 */
00be9182 1157 public static function checkDuplicateContacts(&$fields, &$errors, $contactID, $contactType) {
6a488035 1158 // if this is a forced save, ignore find duplicate rule
a7488080 1159 if (empty($fields['_qf_Contact_upload_duplicate'])) {
6a488035 1160
be2fb01f 1161 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($fields, $contactType, 'Supervised', [$contactID]);
6a488035
TO
1162 if ($ids) {
1163
1164 $contactLinks = CRM_Contact_BAO_Contact_Utils::formatContactIDSToLinks($ids, TRUE, TRUE, $contactID);
1165
1166 $duplicateContactsLinks = '<div class="matching-contacts-found">';
be2fb01f 1167 $duplicateContactsLinks .= ts('One matching contact was found. ', [
69078420
SL
1168 'count' => count($contactLinks['rows']),
1169 'plural' => '%count matching contacts were found.<br />',
1170 ]);
6a488035 1171 if ($contactLinks['msg'] == 'view') {
be2fb01f 1172 $duplicateContactsLinks .= ts('You can View the existing contact', [
69078420
SL
1173 'count' => count($contactLinks['rows']),
1174 'plural' => 'You can View the existing contacts',
1175 ]);
6a488035
TO
1176 }
1177 else {
be2fb01f 1178 $duplicateContactsLinks .= ts('You can View or Edit the existing contact', [
69078420
SL
1179 'count' => count($contactLinks['rows']),
1180 'plural' => 'You can View or Edit the existing contacts',
1181 ]);
6a488035
TO
1182 }
1183 if ($contactLinks['msg'] == 'merge') {
1184 // We should also get a merge link if this is for an existing contact
1185 $duplicateContactsLinks .= ts(', or Merge this contact with an existing contact');
1186 }
1187 $duplicateContactsLinks .= '.';
1188 $duplicateContactsLinks .= '</div>';
1189 $duplicateContactsLinks .= '<table class="matching-contacts-actions">';
1190 $row = '';
1191 for ($i = 0; $i < count($contactLinks['rows']); $i++) {
1192 $row .= ' <tr> ';
1193 $row .= ' <td class="matching-contacts-name"> ';
9a00dcb4 1194 $row .= CRM_Utils_Array::value('display_name', $contactLinks['rows'][$i]);
6a488035
TO
1195 $row .= ' </td>';
1196 $row .= ' <td class="matching-contacts-email"> ';
9a00dcb4 1197 $row .= CRM_Utils_Array::value('primary_email', $contactLinks['rows'][$i]);
6a488035
TO
1198 $row .= ' </td>';
1199 $row .= ' <td class="action-items"> ';
9a00dcb4
TS
1200 $row .= CRM_Utils_Array::value('view', $contactLinks['rows'][$i]);
1201 $row .= CRM_Utils_Array::value('edit', $contactLinks['rows'][$i]);
6a488035
TO
1202 $row .= CRM_Utils_Array::value('merge', $contactLinks['rows'][$i]);
1203 $row .= ' </td>';
1204 $row .= ' </tr> ';
1205 }
1206
1207 $duplicateContactsLinks .= $row . '</table>';
1208 $duplicateContactsLinks .= ts("If you're sure this record is not a duplicate, click the 'Save Matching Contact' button below.");
1209
1210 $errors['_qf_default'] = $duplicateContactsLinks;
1211
6a488035
TO
1212 // let smarty know that there are duplicates
1213 $template = CRM_Core_Smarty::singleton();
1214 $template->assign('isDuplicate', 1);
1215 }
a7488080 1216 elseif (!empty($fields['_qf_Contact_refresh_dedupe'])) {
6a488035
TO
1217 // add a session message for no matching contacts
1218 CRM_Core_Session::setStatus(ts('No matching contact found.'), ts('None Found'), 'info');
1219 }
1220 }
1221 }
1222
86538308 1223 /**
fe482240 1224 * Use the form name to create the tpl file name.
86538308
EM
1225 *
1226 * @return string
86538308 1227 */
00be9182 1228 public function getTemplateFileName() {
6a488035
TO
1229 if ($this->_contactSubType) {
1230 $templateFile = "CRM/Contact/Form/Edit/SubType/{$this->_contactSubType}.tpl";
1231 $template = CRM_Core_Form::getTemplate();
1232 if ($template->template_exists($templateFile)) {
1233 return $templateFile;
1234 }
1235 }
1236 return parent::getTemplateFileName();
1237 }
1238
1239 /**
1240 * Parse all address blocks present in given params
77b97be7
EM
1241 * and return parse result for all address blocks,
1242 * This function either parse street address in to child
1243 * elements or build street address from child elements.
1244 *
5a4f6742
CW
1245 * @param array $params
1246 * of key value consist of address blocks.
77b97be7 1247 *
a6c01b45 1248 * @return array
b44e3f84 1249 * as array of success/fails for each address block
77b97be7 1250 */
00be9182 1251 public function parseAddress(&$params) {
be2fb01f 1252 $parseSuccess = $parsedFields = [];
6a488035
TO
1253 if (!is_array($params['address']) ||
1254 CRM_Utils_System::isNull($params['address'])
1255 ) {
1256 return $parseSuccess;
1257 }
1258
1259 foreach ($params['address'] as $instance => & $address) {
1260 $buildStreetAddress = FALSE;
1261 $parseFieldName = 'street_address';
be2fb01f 1262 foreach ([
69078420
SL
1263 'street_number',
1264 'street_name',
1265 'street_unit',
1266 ] as $fld) {
a7488080 1267 if (!empty($address[$fld])) {
6a488035
TO
1268 $parseFieldName = 'street_number';
1269 $buildStreetAddress = TRUE;
1270 break;
1271 }
1272 }
1273
1274 // main parse string.
1275 $parseString = CRM_Utils_Array::value($parseFieldName, $address);
1276
1277 // parse address field.
1278 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($parseString);
1279
1280 if ($buildStreetAddress) {
1281 //hack to ignore spaces between number and suffix.
1282 //here user gives input as street_number so it has to
1283 //be street_number and street_number_suffix, but
1284 //due to spaces though preg detect string as street_name
1285 //consider it as 'street_number_suffix'.
1286 $suffix = $parsedFields['street_number_suffix'];
1287 if (!$suffix) {
1288 $suffix = $parsedFields['street_name'];
1289 }
1290 $address['street_number_suffix'] = $suffix;
1291 $address['street_number'] = $parsedFields['street_number'];
1292
1293 $streetAddress = NULL;
be2fb01f 1294 foreach ([
69078420
SL
1295 'street_number',
1296 'street_number_suffix',
1297 'street_name',
1298 'street_unit',
1299 ] as $fld) {
be2fb01f 1300 if (in_array($fld, [
353ffa53 1301 'street_name',
af9b09df 1302 'street_unit',
be2fb01f 1303 ])) {
6a488035
TO
1304 $streetAddress .= ' ';
1305 }
9f26d21a 1306 // CRM-17619 - if the street number suffix begins with a number, add a space
92ab8b24
HK
1307 $thesuffix = CRM_Utils_Array::value('street_number_suffix', $address);
1308 if ($fld === 'street_number_suffix' && $thesuffix) {
1309 if (ctype_digit(substr($thesuffix, 0, 1))) {
1310 $streetAddress .= ' ';
1311 }
5e59a285 1312 }
6a488035
TO
1313 $streetAddress .= CRM_Utils_Array::value($fld, $address);
1314 }
1315 $address['street_address'] = trim($streetAddress);
1316 $parseSuccess[$instance] = TRUE;
1317 }
1318 else {
1319 $success = TRUE;
1320 // consider address is automatically parseable,
1321 // when we should found street_number and street_name
8cc574cf 1322 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
6a488035
TO
1323 $success = FALSE;
1324 }
1325
1326 // check for original street address string.
1327 if (empty($parseString)) {
1328 $success = TRUE;
1329 }
1330
1331 $parseSuccess[$instance] = $success;
1332
1333 // we do not reset element values, but keep what we've parsed
1334 // in case of partial matches: CRM-8378
1335
1336 // merge parse address in to main address block.
1337 $address = array_merge($address, $parsedFields);
1338 }
1339 }
1340
1341 return $parseSuccess;
1342 }
1343
1344 /**
100fef9d 1345 * Check parse result and if some address block fails then this
6a488035
TO
1346 * function return the status message for all address blocks.
1347 *
5a4f6742 1348 * @param array $parseResult
77c5b619 1349 * An array of address blk instance and its status.
6a488035 1350 *
72b3a70c
CW
1351 * @return null|string
1352 * $statusMsg string status message for all address blocks.
6a488035 1353 */
00be9182 1354 public static function parseAddressStatusMsg($parseResult) {
6a488035
TO
1355 $statusMsg = NULL;
1356 if (!is_array($parseResult) || empty($parseResult)) {
1357 return $statusMsg;
1358 }
1359
be2fb01f 1360 $parseFails = [];
6a488035
TO
1361 foreach ($parseResult as $instance => $success) {
1362 if (!$success) {
1363 $parseFails[] = self::ordinalNumber($instance);
1364 }
1365 }
1366
1367 if (!empty($parseFails)) {
1368 $statusMsg = ts("Complete street address(es) have been saved. However we were unable to split the address in the %1 address block(s) into address elements (street number, street name, street unit) due to an unrecognized address format. You can set the address elements manually by clicking 'Edit Address Elements' next to the Street Address field while in edit mode.",
be2fb01f 1369 [1 => implode(', ', $parseFails)]
6a488035
TO
1370 );
1371 }
1372
1373 return $statusMsg;
1374 }
1375
1376 /**
1377 * Convert normal number to ordinal number format.
1378 * like 1 => 1st, 2 => 2nd and so on...
1379 *
5a4f6742
CW
1380 * @param int $number
1381 * number to convert in to ordinal number.
6a488035 1382 *
72b3a70c
CW
1383 * @return string
1384 * ordinal number for given number.
6a488035 1385 */
00be9182 1386 public static function ordinalNumber($number) {
6a488035
TO
1387 if (empty($number)) {
1388 return NULL;
1389 }
1390
1391 $str = 'th';
1392 switch (floor($number / 10) % 10) {
1393 case 1:
1394 default:
1395 switch ($number % 10) {
1396 case 1:
1397 $str = 'st';
1398 break;
1399
1400 case 2:
1401 $str = 'nd';
1402 break;
1403
1404 case 3:
1405 $str = 'rd';
1406 break;
1407 }
1408 }
1409
1410 return "$number$str";
1411 }
1412
1413 /**
fe482240 1414 * Update membership status to deceased.
6a488035
TO
1415 * function return the status message for updated membership.
1416 *
5a4f6742
CW
1417 * @param array $deceasedParams
1418 * having contact id and deceased value.
6a488035 1419 *
72b3a70c
CW
1420 * @return null|string
1421 * $updateMembershipMsg string status message for updated membership.
6a488035 1422 */
00be9182 1423 public function updateMembershipStatus($deceasedParams) {
6a488035 1424 $updateMembershipMsg = NULL;
353ffa53
TO
1425 $contactId = CRM_Utils_Array::value('contact_id', $deceasedParams);
1426 $deceasedDate = CRM_Utils_Array::value('deceased_date', $deceasedParams);
6a488035
TO
1427
1428 // process to set membership status to deceased for both active/inactive membership
1429 if ($contactId &&
353ffa53
TO
1430 $this->_contactType == 'Individual' && !empty($deceasedParams['is_deceased'])
1431 ) {
6a488035
TO
1432
1433 $session = CRM_Core_Session::singleton();
1434 $userId = $session->get('userID');
1435 if (!$userId) {
1436 $userId = $contactId;
1437 }
1438
6a488035
TO
1439 // get deceased status id
1440 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
1441 $deceasedStatusId = array_search('Deceased', $allStatus);
1442 if (!$deceasedStatusId) {
1443 return $updateMembershipMsg;
1444 }
1445
1446 $today = time();
1447 if ($deceasedDate && strtotime($deceasedDate) > $today) {
1448 return $updateMembershipMsg;
1449 }
1450
1451 // get non deceased membership
1452 $dao = new CRM_Member_DAO_Membership();
1453 $dao->contact_id = $contactId;
1454 $dao->whereAdd("status_id != $deceasedStatusId");
1455 $dao->find();
1456 $activityTypes = CRM_Core_PseudoConstant::activityType(TRUE, FALSE, FALSE, 'name');
353ffa53 1457 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
6a488035
TO
1458 $memCount = 0;
1459 while ($dao->fetch()) {
1460 // update status to deceased (for both active/inactive membership )
1461 CRM_Core_DAO::setFieldValue('CRM_Member_DAO_Membership', $dao->id,
1462 'status_id', $deceasedStatusId
1463 );
1464
1465 // add membership log
be2fb01f 1466 $membershipLog = [
6a488035
TO
1467 'membership_id' => $dao->id,
1468 'status_id' => $deceasedStatusId,
1469 'start_date' => CRM_Utils_Date::isoToMysql($dao->start_date),
1470 'end_date' => CRM_Utils_Date::isoToMysql($dao->end_date),
1471 'modified_id' => $userId,
1472 'modified_date' => date('Ymd'),
1473 'membership_type_id' => $dao->membership_type_id,
1474 'max_related' => $dao->max_related,
be2fb01f 1475 ];
6a488035 1476
4ed92e91 1477 CRM_Member_BAO_MembershipLog::add($membershipLog);
6a488035
TO
1478
1479 //create activity when membership status is changed
be2fb01f 1480 $activityParam = [
6a488035
TO
1481 'subject' => "Status changed from {$allStatus[$dao->status_id]} to {$allStatus[$deceasedStatusId]}",
1482 'source_contact_id' => $userId,
1483 'target_contact_id' => $dao->contact_id,
1484 'source_record_id' => $dao->id,
1485 'activity_type_id' => array_search('Change Membership Status', $activityTypes),
1486 'status_id' => 2,
1487 'version' => 3,
1488 'priority_id' => 2,
1489 'activity_date_time' => date('Y-m-d H:i:s'),
1490 'is_auto' => 0,
1491 'is_current_revision' => 1,
1492 'is_deleted' => 0,
be2fb01f 1493 ];
6a488035
TO
1494 $activityResult = civicrm_api('activity', 'create', $activityParam);
1495
1496 $memCount++;
1497 }
1498
1499 // set status msg
1500 if ($memCount) {
1501 $updateMembershipMsg = ts("%1 Current membership(s) for this contact have been set to 'Deceased' status.",
be2fb01f 1502 [1 => $memCount]
6a488035
TO
1503 );
1504 }
1505 }
1506
1507 return $updateMembershipMsg;
1508 }
96025800 1509
6a488035 1510}