3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2020 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2020
35 * This class generates form components generic to all the contact types.
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.
42 class CRM_Contact_Form_Contact
extends CRM_Core_Form
{
45 * The contact type of the form.
52 * The contact type of the form.
56 public $_contactSubType;
59 * The contact id, used when editing the form
66 * The default group id passed in via the url.
73 * The default tag id passed in via the url.
80 * Name of de-dupe button
84 protected $_dedupeButtonName;
87 * Name of optional save duplicate button.
91 protected $_duplicateButtonName;
93 protected $_editOptions = [];
95 protected $_oldSubtypes = [];
103 public $_customValueCount;
105 * The array of greetings with option group and filed names.
112 * Do we want to parse street address.
115 public $_parseStreetAddress;
118 * Check contact has a subtype or not.
121 public $_isContactSubType;
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
129 public $_preEditValues;
132 * Explicitly declare the entity api name.
134 public function getDefaultEntity() {
139 * Explicitly declare the form context.
141 public function getDefaultContext() {
146 * Build all the data structures needed to build the form.
148 public function preProcess() {
149 $this->_action
= CRM_Utils_Request
::retrieve('action', 'String', $this, FALSE, 'add');
151 $this->_dedupeButtonName
= $this->getButtonName('refresh', 'dedupe');
152 $this->_duplicateButtonName
= $this->getButtonName('upload', 'duplicate');
154 CRM_Core_Resources
::singleton()
155 ->addStyleFile('civicrm', 'css/contactSummary.css', 2, 'html-header');
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();
164 $this->_contactType
= CRM_Utils_Request
::retrieve('ct', 'String',
165 $this, TRUE, NULL, 'REQUEST'
167 if (!in_array($this->_contactType
,
168 ['Individual', 'Household', 'Organization']
171 CRM_Core_Error
::statusBounce(ts('Could not get a contact id and/or contact type'));
174 $this->_isContactSubType
= FALSE;
175 if ($this->_contactSubType
= CRM_Utils_Request
::retrieve('cst', 'String', $this)) {
176 $this->_isContactSubType
= TRUE;
180 $this->_contactSubType
&&
181 !(CRM_Contact_BAO_ContactType
::isExtendsContactType($this->_contactSubType
, $this->_contactType
, TRUE))
183 CRM_Core_Error
::statusBounce(ts("Could not get a valid contact subtype for contact type '%1'", [1 => $this->_contactType
]));
186 $this->_gid
= CRM_Utils_Request
::retrieve('gid', 'Integer',
187 CRM_Core_DAO
::$_nullObject,
190 $this->_tid
= CRM_Utils_Request
::retrieve('tid', 'Integer',
191 CRM_Core_DAO
::$_nullObject,
194 $typeLabel = CRM_Contact_BAO_ContactType
::contactTypePairs(TRUE, $this->_contactSubType ?
195 $this->_contactSubType
: $this->_contactType
197 $typeLabel = implode(' / ', $typeLabel);
199 CRM_Utils_System
::setTitle(ts('New %1', [1 => $typeLabel]));
200 $session->pushUserContext(CRM_Utils_System
::url('civicrm/dashboard', 'reset=1'));
201 $this->_contactId
= NULL;
205 if (!$this->_contactId
) {
206 $this->_contactId
= CRM_Utils_Request
::retrieve('cid', 'Positive', $this, TRUE);
209 if ($this->_contactId
) {
211 $params = ['id' => $this->_contactId
];
212 $returnProperities = ['id', 'contact_type', 'contact_sub_type', 'modified_date', 'is_deceased'];
213 CRM_Core_DAO
::commonRetrieve('CRM_Contact_DAO_Contact', $params, $defaults, $returnProperities);
215 if (empty($defaults['id'])) {
216 CRM_Core_Error
::statusBounce(ts('A Contact with that ID does not exist: %1', [1 => $this->_contactId
]));
219 $this->_contactType
= CRM_Utils_Array
::value('contact_type', $defaults);
220 $this->_contactSubType
= CRM_Utils_Array
::value('contact_sub_type', $defaults);
222 // check for permissions
223 $session = CRM_Core_Session
::singleton();
224 if (!CRM_Contact_BAO_Contact_Permission
::allow($this->_contactId
, CRM_Core_Permission
::EDIT
)) {
225 CRM_Core_Error
::statusBounce(ts('You do not have the necessary permission to edit this contact.'));
228 $displayName = CRM_Contact_BAO_Contact
::displayName($this->_contactId
);
229 if ($defaults['is_deceased']) {
230 $displayName .= ' <span class="crm-contact-deceased">(' . ts('deceased') . ')</span>';
232 $displayName = ts('Edit %1', [1 => $displayName]);
234 // Check if this is default domain contact CRM-10482
235 if (CRM_Contact_BAO_Contact
::checkDomainContact($this->_contactId
)) {
236 $displayName .= ' (' . ts('default organization') . ')';
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);
241 $context = CRM_Utils_Request
::retrieve('context', 'Alphanumeric', $this);
242 $qfKey = CRM_Utils_Request
::retrieve('key', 'String', $this);
244 $urlParams = 'reset=1&cid=' . $this->_contactId
;
246 $urlParams .= "&context=$context";
249 if (CRM_Utils_Rule
::qfKey($qfKey)) {
251 $urlParams .= "&key=$qfKey";
254 $session->pushUserContext(CRM_Utils_System
::url('civicrm/contact/view', $urlParams));
256 $values = $this->get('values');
257 // get contact values.
258 if (!empty($values)) {
259 $this->_values
= $values;
263 'id' => $this->_contactId
,
264 'contact_id' => $this->_contactId
,
265 'noRelationships' => TRUE,
270 $contact = CRM_Contact_BAO_Contact
::retrieve($params, $this->_values
, TRUE);
271 $this->set('values', $this->_values
);
275 CRM_Core_Error
::statusBounce(ts('Could not get a contact_id and/or contact_type'));
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
,
285 $this->_parseStreetAddress
= FALSE;
286 if (!empty($addressOptions['street_address']) && !empty($addressOptions['street_address_parsing'])) {
287 $this->_parseStreetAddress
= TRUE;
289 $this->set('parseStreetAddress', $this->_parseStreetAddress
);
291 $this->assign('parseStreetAddress', $this->_parseStreetAddress
);
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'
299 $this->set('contactEditOptions', $this->_editOptions
);
302 // build demographics only for Individual contact type
303 if ($this->_contactType
!= 'Individual' &&
304 array_key_exists('Demographics', $this->_editOptions
)
306 unset($this->_editOptions
['Demographics']);
309 // in update mode don't show notes
310 if ($this->_contactId
&& array_key_exists('Notes', $this->_editOptions
)) {
311 unset($this->_editOptions
['Notes']);
314 $this->assign('editOptions', $this->_editOptions
);
315 $this->assign('contactType', $this->_contactType
);
316 $this->assign('contactSubType', $this->_contactSubType
);
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'
325 $this->set('blocks', $this->_blocks
);
327 $this->assign('blocks', $this->_blocks
);
329 // this is needed for custom data.
330 $this->assign('entityID', $this->_contactId
);
332 // also keep the convention.
333 $this->assign('contactId', $this->_contactId
);
336 CRM_Contact_Form_Location
::preProcess($this);
338 // retain the multiple count custom fields value
339 if (!empty($_POST['hidden_custom'])) {
340 $customGroupCount = CRM_Utils_Array
::value('hidden_custom_group_count', $_POST);
342 if ($contactSubType = CRM_Utils_Array
::value('contact_sub_type', $_POST)) {
343 $paramSubType = implode(',', $contactSubType);
346 $this->_getCachedTree
= FALSE;
347 unset($customGroupCount[0]);
348 foreach ($customGroupCount as $groupID => $groupCount) {
349 if ($groupCount > 1) {
350 $this->set('groupID', $groupID);
352 for ($i = 0; $i <= $groupCount; $i++
) {
353 CRM_Custom_Form_CustomData
::preProcess($this, NULL, $contactSubType,
354 $i, $this->_contactType
, $this->_contactId
356 CRM_Contact_Form_Edit_CustomData
::buildQuickForm($this);
361 //reset all the ajax stuff, for normal processing
362 if (isset($this->_groupTree
)) {
363 $this->_groupTree
= NULL;
365 $this->set('groupID', NULL);
366 $this->_getCachedTree
= TRUE;
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 :
375 str_replace(CRM_Core_DAO
::VALUE_SEPARATOR
, ',', trim($this->_contactSubType
, CRM_Core_DAO
::VALUE_SEPARATOR
));
377 $this->assign('paramSubType', $paramSubType);
380 if (CRM_Utils_Request
::retrieve('type', 'String')) {
381 CRM_Contact_Form_Edit_CustomData
::preProcess($this);
384 $contactSubType = $this->_contactSubType
;
385 // need contact sub type to build related grouptree array during post process
386 if (!empty($_POST['contact_sub_type'])) {
387 $contactSubType = $_POST['contact_sub_type'];
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
393 $this->assign('customValueCount', $this->_customValueCount
);
399 * Set default values for the form.
401 * Note that in edit/view mode the default values are retrieved from the database
403 public function setDefaultValues() {
404 $defaults = $this->_values
;
406 if ($this->_action
& CRM_Core_Action
::ADD
) {
407 if (array_key_exists('TagsAndGroups', $this->_editOptions
)) {
408 // set group and tag defaults if any
410 $defaults['group'][] = $this->_gid
;
413 $defaults['tag'][$this->_tid
] = 1;
416 if ($this->_contactSubType
) {
417 $defaults['contact_sub_type'] = $this->_contactSubType
;
421 foreach ($defaults['email'] as $dontCare => & $val) {
422 if (isset($val['signature_text'])) {
423 $val['signature_text_hidden'] = $val['signature_text'];
425 if (isset($val['signature_html'])) {
426 $val['signature_html_hidden'] = $val['signature_html'];
430 if (!empty($defaults['contact_sub_type'])) {
431 $defaults['contact_sub_type'] = $this->_oldSubtypes
;
434 // set defaults for blocks ( custom data, address, communication preference, notes, tags and groups )
435 foreach ($this->_editOptions
as $name => $label) {
436 if (!in_array($name, ['Address', 'Notes'])) {
437 $className = 'CRM_Contact_Form_Edit_' . $name;
438 $className::setDefaultValues($this, $defaults);
442 //set address block defaults
443 CRM_Contact_Form_Edit_Address
::setDefaultValues($defaults, $this);
445 if (!empty($defaults['image_URL'])) {
446 $this->assign("imageURL", CRM_Utils_File
::getImageURL($defaults['image_URL']));
449 //set location type and country to default for each block
450 $this->blockSetDefaults($defaults);
452 $this->_preEditValues
= $defaults;
457 * Do the set default related to location type id, primary location, default country.
459 * @param array $defaults
461 public function blockSetDefaults(&$defaults) {
462 $locationTypeKeys = array_filter(array_keys(CRM_Core_PseudoConstant
::get('CRM_Core_DAO_Address', 'location_type_id')), 'is_int');
463 sort($locationTypeKeys);
465 // get the default location type
466 $locationType = CRM_Core_BAO_LocationType
::getDefault();
468 // unset primary location type
469 $primaryLocationTypeIdKey = CRM_Utils_Array
::key($locationType->id
, $locationTypeKeys);
470 unset($locationTypeKeys[$primaryLocationTypeIdKey]);
472 // reset the array sequence
473 $locationTypeKeys = array_values($locationTypeKeys);
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',
478 FALSE, FALSE, FALSE, ' AND is_default = 1'
480 $defWebsiteTypeId = key(CRM_Core_OptionGroup
::values('website_type',
481 FALSE, FALSE, FALSE, ' AND is_default = 1'
484 $allBlocks = $this->_blocks
;
485 if (array_key_exists('Address', $this->_editOptions
)) {
486 $allBlocks['Address'] = $this->_editOptions
['Address'];
489 $config = CRM_Core_Config
::singleton();
490 foreach ($allBlocks as $blockName => $label) {
491 $name = strtolower($blockName);
492 $hasPrimary = $updateMode = FALSE;
494 // user is in update mode.
495 if (array_key_exists($name, $defaults) &&
496 !CRM_Utils_System
::isNull($defaults[$name])
501 for ($instance = 1; $instance <= $this->get($blockName . '_Block_Count'); $instance++
) {
502 // make we require one primary block, CRM-5505
505 $hasPrimary = CRM_Utils_Array
::value(
507 CRM_Utils_Array
::value($instance, $defaults[$name])
513 //set location to primary for first one.
514 if ($instance == 1) {
516 $defaults[$name][$instance]['is_primary'] = TRUE;
517 $defaults[$name][$instance]['location_type_id'] = $locationType->id
;
520 $locTypeId = isset($locationTypeKeys[$instance - 1]) ?
$locationTypeKeys[$instance - 1] : $locationType->id
;
521 $defaults[$name][$instance]['location_type_id'] = $locTypeId;
524 //set default country
525 if ($name == 'address' && $config->defaultContactCountry
) {
526 $defaults[$name][$instance]['country_id'] = $config->defaultContactCountry
;
529 //set default state/province
530 if ($name == 'address' && $config->defaultContactStateProvince
) {
531 $defaults[$name][$instance]['state_province_id'] = $config->defaultContactStateProvince
;
534 //set default phone type.
535 if ($name == 'phone' && $defPhoneTypeId) {
536 $defaults[$name][$instance]['phone_type_id'] = $defPhoneTypeId;
538 //set default website type.
539 if ($name == 'website' && $defWebsiteTypeId) {
540 $defaults[$name][$instance]['website_type_id'] = $defWebsiteTypeId;
543 //set default im provider.
544 if ($name == 'im' && $defIMProviderId) {
545 $defaults[$name][$instance]['provider_id'] = $defIMProviderId;
550 $defaults[$name][1]['is_primary'] = TRUE;
556 * add the rules (mainly global rules) for form.
557 * All local rules are added near the element
561 public function addRules() {
562 // skip adding formRules when custom data is build
563 if ($this->_addBlockName ||
($this->_action
& CRM_Core_Action
::DELETE
)) {
567 $this->addFormRule(['CRM_Contact_Form_Edit_' . $this->_contactType
, 'formRule'], $this->_contactId
);
569 // Call Locking check if editing existing contact
570 if ($this->_contactId
) {
571 $this->addFormRule(['CRM_Contact_Form_Edit_Lock', 'formRule'], $this->_contactId
);
574 if (array_key_exists('Address', $this->_editOptions
)) {
575 $this->addFormRule(['CRM_Contact_Form_Edit_Address', 'formRule'], $this);
578 if (array_key_exists('CommunicationPreferences', $this->_editOptions
)) {
579 $this->addFormRule(['CRM_Contact_Form_Edit_CommunicationPreferences', 'formRule'], $this);
584 * Global validation rules for the form.
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.
592 * @param string $contactType
597 public static function formRule($fields, &$errors, $contactId, $contactType) {
598 $config = CRM_Core_Config
::singleton();
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.
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'
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'
617 //get address block inside.
618 if (array_key_exists('Address', $otherEditOptions)) {
619 $blocks['Address'] = $otherEditOptions['Address'];
625 foreach ($blocks as $name => $label) {
626 $hasData = $hasPrimary = [];
627 $name = strtolower($name);
628 if (!empty($fields[$name]) && is_array($fields[$name])) {
629 foreach ($fields[$name] as $instance => $blockValues) {
630 $dataExists = self
::blockDataExists($blockValues);
632 if (!$dataExists && $name == 'address') {
633 $dataExists = CRM_Utils_Array
::value('use_shared_address', $fields['address'][$instance]);
637 if ($name == 'website') {
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'];
643 $errors["{$name}[1][website_type_id]"] = ts('Contacts may only have one website of each type at most.');
647 // skip remaining checks for website
651 $hasData[] = $instance;
652 if (!empty($blockValues['is_primary'])) {
653 $hasPrimary[] = $instance;
658 ]) && !empty($blockValues[$name])
660 $primaryID = $blockValues[$name];
664 if (empty($blockValues['location_type_id'])) {
665 $errors["{$name}[$instance][location_type_id]"] = ts('The Location Type should be set if there is %1 information.', [1 => $label]);
669 if ($name == 'openid' && !empty($blockValues[$name])) {
670 $oid = new CRM_Core_DAO_OpenID();
671 $oid->openid
= $openIds[$instance] = CRM_Utils_Array
::value($name, $blockValues);
672 $cid = isset($contactId) ?
$contactId : 0;
673 if ($oid->find(TRUE) && ($oid->contact_id
!= $cid)) {
674 $errors["{$name}[$instance][openid]"] = ts('%1 already exist.', [1 => $blocks['OpenID']]);
679 if (empty($hasPrimary) && !empty($hasData)) {
680 $errors["{$name}[1][is_primary]"] = ts('One %1 should be marked as primary.', [1 => $label]);
683 if (count($hasPrimary) > 1) {
684 $errors["{$name}[" . array_pop($hasPrimary) . "][is_primary]"] = ts('Only one %1 can be marked as primary.',
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))) {
695 $errors["openid[$instance][openid]"] = ts('%1 already used.', [1 => $blocks['OpenID']]);
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
,
706 if ($parseStreetAddress) {
707 if (isset($fields['address']) &&
708 is_array($fields['address'])
710 $invalidStreetNumbers = [];
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']);
714 if (empty($parsedAddress['street_number'])) {
715 $invalidStreetNumbers[] = $cnt;
720 if (!empty($invalidStreetNumbers)) {
721 $first = $invalidStreetNumbers[0];
722 foreach ($invalidStreetNumbers as & $num) {
723 $num = CRM_Contact_Form_Contact
::ordinalNumber($num);
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)]);
730 // Check for duplicate contact if it wasn't already handled by ajax or disabled
731 if (!Civi
::settings()->get('contact_ajax_check_similar') ||
!empty($fields['_qf_Contact_refresh_dedupe'])) {
732 self
::checkDuplicateContacts($fields, $errors, $contactId, $contactType);
739 * Build the form object.
741 public function buildQuickForm() {
742 //load form for child blocks
743 if ($this->_addBlockName
) {
744 $className = 'CRM_Contact_Form_Edit_' . $this->_addBlockName
;
745 return $className::buildQuickForm($this);
748 if ($this->_action
== CRM_Core_Action
::UPDATE
) {
749 $deleteExtra = json_encode(ts('Are you sure you want to delete contact image.'));
751 CRM_Core_Action
::DELETE
=> [
752 'name' => ts('Delete Contact Image'),
753 'url' => 'civicrm/contact/image',
754 'qs' => 'reset=1&cid=%%id%%&action=delete',
755 'extra' => 'onclick = "' . htmlspecialchars("if (confirm($deleteExtra)) this.href+='&confirmed=1'; else return false;") . '"',
758 $deleteURL = CRM_Core_Action
::formLink($deleteURL,
759 CRM_Core_Action
::DELETE
,
761 'id' => $this->_contactId
,
765 'contact.image.delete',
769 $this->assign('deleteURL', $deleteURL);
772 //build contact type specific fields
773 $className = 'CRM_Contact_Form_Edit_' . $this->_contactType
;
774 $className::buildQuickForm($this);
776 // Ajax duplicate checking
777 $checkSimilar = Civi
::settings()->get('contact_ajax_check_similar');
778 $this->assign('checkSimilar', $checkSimilar);
779 if ($checkSimilar == 1) {
780 $ruleParams = ['used' => 'Supervised', 'contact_type' => $this->_contactType
];
781 $this->assign('ruleFields', CRM_Dedupe_BAO_Rule
::dedupeRuleFields($ruleParams));
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";
790 // subtype is a common field. lets keep it here
791 $subtypes = CRM_Contact_BAO_Contact
::buildOptions('contact_sub_type', 'create', ['contact_type' => $this->_contactType
]);
792 if (!empty($subtypes)) {
793 $this->addField('contact_sub_type', [
794 'label' => ts('Contact Type'),
795 'options' => $subtypes,
796 'class' => $buildCustomData,
797 'multiple' => 'multiple',
798 'option_url' => NULL,
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'];
808 if ($name == 'TagsAndGroups') {
811 $className = 'CRM_Contact_Form_Edit_' . $name;
812 $className::buildQuickForm($this);
815 // build tags and groups
816 CRM_Contact_Form_Edit_TagsAndGroups
::buildQuickForm($this, 0, CRM_Contact_Form_Edit_TagsAndGroups
::ALL
,
817 FALSE, NULL, 'Group(s)', 'Tag(s)', NULL, 'select');
819 // build location blocks.
820 CRM_Contact_Form_Edit_Lock
::buildQuickForm($this);
821 CRM_Contact_Form_Location
::buildQuickForm($this);
824 $this->addField('image_URL', ['maxlength' => '255', 'label' => ts('Browse/Upload Image')]);
826 // add the dedupe button
827 $this->addElement('submit',
828 $this->_dedupeButtonName
,
829 ts('Check for Matching Contact(s)')
831 $this->addElement('submit',
832 $this->_duplicateButtonName
,
833 ts('Save Matching Contact')
835 $this->addElement('submit',
836 $this->getButtonName('next', 'sharedHouseholdDuplicate'),
837 ts('Save With Duplicate Household')
843 'name' => ts('Save'),
848 if (CRM_Core_Permission
::check('add contacts')) {
851 'name' => ts('Save and New'),
852 'spacing' => ' ',
858 'name' => ts('Cancel'),
861 if (!empty($this->_values
['contact_sub_type'])) {
862 $this->_oldSubtypes
= explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
863 trim($this->_values
['contact_sub_type'], CRM_Core_DAO
::VALUE_SEPARATOR
)
866 $this->assign('oldSubtypes', json_encode($this->_oldSubtypes
));
868 $this->addButtons($buttons);
872 * Form submission of new/edit contact is processed.
874 public function postProcess() {
875 // check if dedupe button, if so return.
876 $buttonName = $this->controller
->getButtonName();
877 if ($buttonName == $this->_dedupeButtonName
) {
881 //get the submitted values in an array
882 $params = $this->controller
->exportValues($this->_name
);
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';
888 $group = CRM_Utils_Array
::value('group', $params);
889 if (!empty($group) && is_array($group)) {
890 unset($params['group']);
891 foreach ($group as $key => $value) {
892 $params['group'][$value] = 1;
896 if (!empty($params['image_URL'])) {
897 CRM_Contact_BAO_Contact
::processImageParams($params);
900 if (is_numeric(CRM_Utils_Array
::value('current_employer_id', $params)) && !empty($params['current_employer'])) {
901 $params['current_employer'] = $params['current_employer_id'];
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']);
911 $params['contact_type'] = $this->_contactType
;
912 if (empty($params['contact_sub_type']) && $this->_isContactSubType
) {
913 $params['contact_sub_type'] = [$this->_contactSubType
];
916 if ($this->_contactId
) {
917 $params['contact_id'] = $this->_contactId
;
920 //make deceased date null when is_deceased = false
921 if ($this->_contactType
== 'Individual' && !empty($this->_editOptions
['Demographics']) && empty($params['is_deceased'])) {
922 $params['is_deceased'] = FALSE;
923 $params['deceased_date'] = NULL;
926 if (isset($params['contact_id'])) {
927 // process membership status for deceased contact
929 'contact_id' => CRM_Utils_Array
::value('contact_id', $params),
930 'is_deceased' => CRM_Utils_Array
::value('is_deceased', $params, FALSE),
931 'deceased_date' => CRM_Utils_Array
::value('deceased_date', $params, NULL),
933 $updateMembershipMsg = $this->updateMembershipStatus($deceasedParams);
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);
941 CRM_Utils_Hook
::pre('create', $params['contact_type'], NULL, $params);
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'];
948 $params['custom'] = CRM_Core_BAO_CustomField
::postProcess($params,
953 if ($this->_contactId
&& !empty($this->_oldSubtypes
)) {
954 CRM_Contact_BAO_ContactType
::deleteCustomSetForSubtypeMigration($this->_contactId
,
955 $params['contact_type'],
957 $params['contact_sub_type']
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);
966 // process shared contact address.
967 CRM_Contact_BAO_Contact_Utils
::processSharedAddress($params['address']);
969 if (!array_key_exists('TagsAndGroups', $this->_editOptions
)) {
970 unset($params['group']);
972 elseif (!empty($params['contact_id']) && ($this->_action
& CRM_Core_Action
::UPDATE
)) {
973 // figure out which all groups are intended to be removed
974 $contactGroupList = CRM_Contact_BAO_GroupContact
::getContactGroup($params['contact_id'], 'Added');
975 if (is_array($contactGroupList)) {
976 foreach ($contactGroupList as $key) {
977 if ((!array_key_exists($key['group_id'], $params['group']) ||
$params['group'][$key['group_id']] != 1) && empty($key['is_hidden'])) {
978 $params['group'][$key['group_id']] = -1;
984 // parse street address, CRM-5450
985 $parseStatusMsg = NULL;
986 if ($this->_parseStreetAddress
) {
987 $parseResult = self
::parseAddress($params);
988 $parseStatusMsg = self
::parseAddressStatusMsg($parseResult);
991 $blocks = ['email', 'phone', 'im', 'openid', 'address', 'website'];
992 foreach ($blocks as $block) {
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;
1003 // Allow un-setting of location info, CRM-5969
1004 $params['updateBlankLocInfo'] = TRUE;
1006 $contact = CRM_Contact_BAO_Contact
::create($params, TRUE, FALSE, TRUE);
1009 if ($this->_contactId
) {
1010 $message = ts('%1 has been updated.', [1 => $contact->display_name
]);
1013 $message = ts('%1 has been created.', [1 => $contact->display_name
]);
1016 // set the contact ID
1017 $this->_contactId
= $contact->id
;
1019 if (array_key_exists('TagsAndGroups', $this->_editOptions
)) {
1020 //add contact to tags
1021 if (isset($params['tag'])) {
1022 $params['tag'] = array_flip(explode(',', $params['tag']));
1023 CRM_Core_BAO_EntityTag
::create($params['tag'], 'civicrm_contact', $params['contact_id']);
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);
1031 if (!empty($parseStatusMsg)) {
1032 $message .= "<br />$parseStatusMsg";
1034 if (!empty($updateMembershipMsg)) {
1035 $message .= "<br />$updateMembershipMsg";
1038 $session = CRM_Core_Session
::singleton();
1039 $session->setStatus($message, ts('Contact Saved'), 'success');
1041 // add the recently viewed contact
1043 if (($session->get('userID') == $contact->id
) ||
1044 CRM_Contact_BAO_Contact_Permission
::allow($contact->id
, CRM_Core_Permission
::EDIT
)
1046 $recentOther['editUrl'] = CRM_Utils_System
::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contact->id
);
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
);
1053 CRM_Utils_Recent
::add($contact->display_name
,
1054 CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $contact->id
),
1056 $this->_contactType
,
1058 $contact->display_name
,
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')) {
1065 $contactSubTypes = array_filter(explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $this->_contactSubType
));
1066 $resetStr = "reset=1&ct={$contact->contact_type}";
1067 $resetStr .= (count($contactSubTypes) == 1) ?
"&cst=" . array_pop($contactSubTypes) : '';
1068 $session->replaceUserContext(CRM_Utils_System
::url('civicrm/contact/add', $resetStr));
1071 $context = CRM_Utils_Request
::retrieve('context', 'Alphanumeric', $this);
1072 $qfKey = CRM_Utils_Request
::retrieve('key', 'String', $this);
1073 //validate the qfKey
1074 $urlParams = 'reset=1&cid=' . $contact->id
;
1076 $urlParams .= "&context=$context";
1078 if (CRM_Utils_Rule
::qfKey($qfKey)) {
1079 $urlParams .= "&key=$qfKey";
1082 $session->replaceUserContext(CRM_Utils_System
::url('civicrm/contact/view', $urlParams));
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);
1090 CRM_Utils_Hook
::post('create', $params['contact_type'], $contact->id
, $contact);
1095 * Is there any real significant data in the hierarchical location array.
1097 * @param array $fields
1098 * The hierarchical value representation of this location.
1101 * true if data exists, false otherwise
1103 public static function blockDataExists(&$fields) {
1104 if (!is_array($fields)) {
1108 static $skipFields = [
1117 foreach ($fields as $name => $value) {
1119 foreach ($skipFields as $skip) {
1120 if (strpos("[$skip]", $name) !== FALSE) {
1121 if ($name == 'phone') {
1131 if (is_array($value)) {
1132 if (self
::blockDataExists($value)) {
1137 if (!empty($value)) {
1147 * That checks for duplicate contacts.
1149 * @param array $fields
1150 * Fields array which are submitted.
1152 * @param int $contactID
1154 * @param string $contactType
1157 public static function checkDuplicateContacts(&$fields, &$errors, $contactID, $contactType) {
1158 // if this is a forced save, ignore find duplicate rule
1159 if (empty($fields['_qf_Contact_upload_duplicate'])) {
1161 $ids = CRM_Contact_BAO_Contact
::getDuplicateContacts($fields, $contactType, 'Supervised', [$contactID]);
1164 $contactLinks = CRM_Contact_BAO_Contact_Utils
::formatContactIDSToLinks($ids, TRUE, TRUE, $contactID);
1166 $duplicateContactsLinks = '<div class="matching-contacts-found">';
1167 $duplicateContactsLinks .= ts('One matching contact was found. ', [
1168 'count' => count($contactLinks['rows']),
1169 'plural' => '%count matching contacts were found.<br />',
1171 if ($contactLinks['msg'] == 'view') {
1172 $duplicateContactsLinks .= ts('You can View the existing contact', [
1173 'count' => count($contactLinks['rows']),
1174 'plural' => 'You can View the existing contacts',
1178 $duplicateContactsLinks .= ts('You can View or Edit the existing contact', [
1179 'count' => count($contactLinks['rows']),
1180 'plural' => 'You can View or Edit the existing contacts',
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');
1187 $duplicateContactsLinks .= '.';
1188 $duplicateContactsLinks .= '</div>';
1189 $duplicateContactsLinks .= '<table class="matching-contacts-actions">';
1191 for ($i = 0; $i < count($contactLinks['rows']); $i++
) {
1193 $row .= ' <td class="matching-contacts-name"> ';
1194 $row .= CRM_Utils_Array
::value('display_name', $contactLinks['rows'][$i]);
1196 $row .= ' <td class="matching-contacts-email"> ';
1197 $row .= CRM_Utils_Array
::value('primary_email', $contactLinks['rows'][$i]);
1199 $row .= ' <td class="action-items"> ';
1200 $row .= CRM_Utils_Array
::value('view', $contactLinks['rows'][$i]);
1201 $row .= CRM_Utils_Array
::value('edit', $contactLinks['rows'][$i]);
1202 $row .= CRM_Utils_Array
::value('merge', $contactLinks['rows'][$i]);
1207 $duplicateContactsLinks .= $row . '</table>';
1208 $duplicateContactsLinks .= ts("If you're sure this record is not a duplicate, click the 'Save Matching Contact' button below.");
1210 $errors['_qf_default'] = $duplicateContactsLinks;
1212 // let smarty know that there are duplicates
1213 $template = CRM_Core_Smarty
::singleton();
1214 $template->assign('isDuplicate', 1);
1216 elseif (!empty($fields['_qf_Contact_refresh_dedupe'])) {
1217 // add a session message for no matching contacts
1218 CRM_Core_Session
::setStatus(ts('No matching contact found.'), ts('None Found'), 'info');
1224 * Use the form name to create the tpl file name.
1228 public function getTemplateFileName() {
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;
1236 return parent
::getTemplateFileName();
1240 * Parse all address blocks present in given params
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.
1245 * @param array $params
1246 * of key value consist of address blocks.
1249 * as array of success/fails for each address block
1251 public function parseAddress(&$params) {
1252 $parseSuccess = $parsedFields = [];
1253 if (!is_array($params['address']) ||
1254 CRM_Utils_System
::isNull($params['address'])
1256 return $parseSuccess;
1259 foreach ($params['address'] as $instance => & $address) {
1260 $buildStreetAddress = FALSE;
1261 $parseFieldName = 'street_address';
1267 if (!empty($address[$fld])) {
1268 $parseFieldName = 'street_number';
1269 $buildStreetAddress = TRUE;
1274 // main parse string.
1275 $parseString = CRM_Utils_Array
::value($parseFieldName, $address);
1277 // parse address field.
1278 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($parseString);
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'];
1288 $suffix = $parsedFields['street_name'];
1290 $address['street_number_suffix'] = $suffix;
1291 $address['street_number'] = $parsedFields['street_number'];
1293 $streetAddress = NULL;
1296 'street_number_suffix',
1300 if (in_array($fld, [
1304 $streetAddress .= ' ';
1306 // CRM-17619 - if the street number suffix begins with a number, add a space
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 .= ' ';
1313 $streetAddress .= CRM_Utils_Array
::value($fld, $address);
1315 $address['street_address'] = trim($streetAddress);
1316 $parseSuccess[$instance] = TRUE;
1320 // consider address is automatically parseable,
1321 // when we should found street_number and street_name
1322 if (empty($parsedFields['street_name']) ||
empty($parsedFields['street_number'])) {
1326 // check for original street address string.
1327 if (empty($parseString)) {
1331 $parseSuccess[$instance] = $success;
1333 // we do not reset element values, but keep what we've parsed
1334 // in case of partial matches: CRM-8378
1336 // merge parse address in to main address block.
1337 $address = array_merge($address, $parsedFields);
1341 return $parseSuccess;
1345 * Check parse result and if some address block fails then this
1346 * function return the status message for all address blocks.
1348 * @param array $parseResult
1349 * An array of address blk instance and its status.
1351 * @return null|string
1352 * $statusMsg string status message for all address blocks.
1354 public static function parseAddressStatusMsg($parseResult) {
1356 if (!is_array($parseResult) ||
empty($parseResult)) {
1361 foreach ($parseResult as $instance => $success) {
1363 $parseFails[] = self
::ordinalNumber($instance);
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.",
1369 [1 => implode(', ', $parseFails)]
1377 * Convert normal number to ordinal number format.
1378 * like 1 => 1st, 2 => 2nd and so on...
1380 * @param int $number
1381 * number to convert in to ordinal number.
1384 * ordinal number for given number.
1386 public static function ordinalNumber($number) {
1387 if (empty($number)) {
1392 switch (floor($number / 10) %
10) {
1395 switch ($number %
10) {
1410 return "$number$str";
1414 * Update membership status to deceased.
1415 * function return the status message for updated membership.
1417 * @param array $deceasedParams
1418 * having contact id and deceased value.
1420 * @return null|string
1421 * $updateMembershipMsg string status message for updated membership.
1423 public function updateMembershipStatus($deceasedParams) {
1424 $updateMembershipMsg = NULL;
1425 $contactId = CRM_Utils_Array
::value('contact_id', $deceasedParams);
1426 $deceasedDate = CRM_Utils_Array
::value('deceased_date', $deceasedParams);
1428 // process to set membership status to deceased for both active/inactive membership
1430 $this->_contactType
== 'Individual' && !empty($deceasedParams['is_deceased'])
1433 $session = CRM_Core_Session
::singleton();
1434 $userId = $session->get('userID');
1436 $userId = $contactId;
1439 // get deceased status id
1440 $allStatus = CRM_Member_PseudoConstant
::membershipStatus();
1441 $deceasedStatusId = array_search('Deceased', $allStatus);
1442 if (!$deceasedStatusId) {
1443 return $updateMembershipMsg;
1447 if ($deceasedDate && strtotime($deceasedDate) > $today) {
1448 return $updateMembershipMsg;
1451 // get non deceased membership
1452 $dao = new CRM_Member_DAO_Membership();
1453 $dao->contact_id
= $contactId;
1454 $dao->whereAdd("status_id != $deceasedStatusId");
1456 $activityTypes = CRM_Core_PseudoConstant
::activityType(TRUE, FALSE, FALSE, 'name');
1457 $allStatus = CRM_Member_PseudoConstant
::membershipStatus();
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
1465 // add membership log
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
,
1477 CRM_Member_BAO_MembershipLog
::add($membershipLog);
1479 //create activity when membership status is changed
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),
1489 'activity_date_time' => date('Y-m-d H:i:s'),
1491 'is_current_revision' => 1,
1494 $activityResult = civicrm_api('activity', 'create', $activityParam);
1501 $updateMembershipMsg = ts("%1 Current membership(s) for this contact have been set to 'Deceased' status.",
1507 return $updateMembershipMsg;