Update copyright date for 2020
[civicrm-core.git] / CRM / Contact / Form / Contact.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2020 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2020
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 */
42 class CRM_Contact_Form_Contact extends CRM_Core_Form {
43
44 /**
45 * The contact type of the form.
46 *
47 * @var string
48 */
49 public $_contactType;
50
51 /**
52 * The contact type of the form.
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 /**
66 * The default group id passed in via the url.
67 *
68 * @var int
69 */
70 public $_gid;
71
72 /**
73 * The default tag id passed in via the url.
74 *
75 * @var int
76 */
77 public $_tid;
78
79 /**
80 * Name of de-dupe button
81 *
82 * @var string
83 */
84 protected $_dedupeButtonName;
85
86 /**
87 * Name of optional save duplicate button.
88 *
89 * @var string
90 */
91 protected $_duplicateButtonName;
92
93 protected $_editOptions = [];
94
95 protected $_oldSubtypes = [];
96
97 public $_blocks;
98
99 public $_values = [];
100
101 public $_action;
102
103 public $_customValueCount;
104 /**
105 * The array of greetings with option group and filed names.
106 *
107 * @var array
108 */
109 public $_greetings;
110
111 /**
112 * Do we want to parse street address.
113 * @var bool
114 */
115 public $_parseStreetAddress;
116
117 /**
118 * Check contact has a subtype or not.
119 * @var bool
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
127 * @var array
128 */
129 public $_preEditValues;
130
131 /**
132 * Explicitly declare the entity api name.
133 */
134 public function getDefaultEntity() {
135 return 'Contact';
136 }
137
138 /**
139 * Explicitly declare the form context.
140 */
141 public function getDefaultContext() {
142 return 'create';
143 }
144
145 /**
146 * Build all the data structures needed to build the form.
147 */
148 public function preProcess() {
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
154 CRM_Core_Resources::singleton()
155 ->addStyleFile('civicrm', 'css/contactSummary.css', 2, 'html-header');
156
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,
168 ['Individual', 'Household', 'Organization']
169 )
170 ) {
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 &&
181 !(CRM_Contact_BAO_ContactType::isExtendsContactType($this->_contactSubType, $this->_contactType, TRUE))
182 ) {
183 CRM_Core_Error::statusBounce(ts("Could not get a valid contact subtype for contact type '%1'", [1 => $this->_contactType]));
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 ?
195 $this->_contactSubType : $this->_contactType
196 );
197 $typeLabel = implode(' / ', $typeLabel);
198
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;
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) {
210 $defaults = [];
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);
214
215 if (empty($defaults['id'])) {
216 CRM_Core_Error::statusBounce(ts('A Contact with that ID does not exist: %1', [1 => $this->_contactId]));
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();
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.'));
226 }
227
228 $displayName = CRM_Contact_BAO_Contact::displayName($this->_contactId);
229 if ($defaults['is_deceased']) {
230 $displayName .= ' <span class="crm-contact-deceased">(' . ts('deceased') . ')</span>';
231 }
232 $displayName = ts('Edit %1', [1 => $displayName]);
233
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 }
238
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);
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 {
262 $params = [
263 'id' => $this->_contactId,
264 'contact_id' => $this->_contactId,
265 'noRelationships' => TRUE,
266 'noNotes' => TRUE,
267 'noGroups' => TRUE,
268 ];
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;
286 if (!empty($addressOptions['street_address']) && !empty($addressOptions['street_address_parsing'])) {
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
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
339 if (!empty($_POST['hidden_custom'])) {
340 $customGroupCount = CRM_Utils_Array::value('hidden_custom_group_count', $_POST);
341
342 if ($contactSubType = CRM_Utils_Array::value('contact_sub_type', $_POST)) {
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,
354 $i, $this->_contactType, $this->_contactId
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 :
375 str_replace(CRM_Core_DAO::VALUE_SEPARATOR, ',', trim($this->_contactSubType, CRM_Core_DAO::VALUE_SEPARATOR));
376
377 $this->assign('paramSubType', $paramSubType);
378 }
379
380 if (CRM_Utils_Request::retrieve('type', 'String')) {
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
386 if (!empty($_POST['contact_sub_type'])) {
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 /**
399 * Set default values for the form.
400 *
401 * Note that in edit/view mode the default values are retrieved from the database
402 */
403 public function setDefaultValues() {
404 $defaults = $this->_values;
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) {
410 $defaults['group'][] = $this->_gid;
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 {
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
430 if (!empty($defaults['contact_sub_type'])) {
431 $defaults['contact_sub_type'] = $this->_oldSubtypes;
432 }
433 }
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);
439 }
440 }
441
442 //set address block defaults
443 CRM_Contact_Form_Edit_Address::setDefaultValues($defaults, $this);
444
445 if (!empty($defaults['image_URL'])) {
446 $this->assign("imageURL", CRM_Utils_File::getImageURL($defaults['image_URL']));
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 /**
457 * Do the set default related to location type id, primary location, default country.
458 *
459 * @param array $defaults
460 */
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);
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',
478 FALSE, FALSE, FALSE, ' AND is_default = 1'
479 ));
480 $defWebsiteTypeId = key(CRM_Core_OptionGroup::values('website_type',
481 FALSE, FALSE, FALSE, ' AND is_default = 1'
482 ));
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
503 if ($updateMode) {
504 if (!$hasPrimary) {
505 $hasPrimary = CRM_Utils_Array::value(
506 'is_primary',
507 CRM_Utils_Array::value($instance, $defaults[$name])
508 );
509 }
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 }
538 //set default website type.
539 if ($name == 'website' && $defWebsiteTypeId) {
540 $defaults[$name][$instance]['website_type_id'] = $defWebsiteTypeId;
541 }
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 }
553 }
554
555 /**
556 * add the rules (mainly global rules) for form.
557 * All local rules are added near the element
558 *
559 * @see valid_date
560 */
561 public function addRules() {
562 // skip adding formRules when custom data is build
563 if ($this->_addBlockName || ($this->_action & CRM_Core_Action::DELETE)) {
564 return;
565 }
566
567 $this->addFormRule(['CRM_Contact_Form_Edit_' . $this->_contactType, 'formRule'], $this->_contactId);
568
569 // Call Locking check if editing existing contact
570 if ($this->_contactId) {
571 $this->addFormRule(['CRM_Contact_Form_Edit_Lock', 'formRule'], $this->_contactId);
572 }
573
574 if (array_key_exists('Address', $this->_editOptions)) {
575 $this->addFormRule(['CRM_Contact_Form_Edit_Address', 'formRule'], $this);
576 }
577
578 if (array_key_exists('CommunicationPreferences', $this->_editOptions)) {
579 $this->addFormRule(['CRM_Contact_Form_Edit_CommunicationPreferences', 'formRule'], $this);
580 }
581 }
582
583 /**
584 * Global validation rules for the form.
585 *
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
593 *
594 * @return bool
595 * email/openId
596 */
597 public static function formRule($fields, &$errors, $contactId, $contactType) {
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
622 $website_types = [];
623 $openIds = [];
624 $primaryID = FALSE;
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);
631
632 if (!$dataExists && $name == 'address') {
633 $dataExists = CRM_Utils_Array::value('use_shared_address', $fields['address'][$instance]);
634 }
635
636 if ($dataExists) {
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'];
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
648 continue;
649 }
650
651 $hasData[] = $instance;
652 if (!empty($blockValues['is_primary'])) {
653 $hasPrimary[] = $instance;
654 if (!$primaryID &&
655 in_array($name, [
656 'email',
657 'openid',
658 ]) && !empty($blockValues[$name])
659 ) {
660 $primaryID = $blockValues[$name];
661 }
662 }
663
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]);
666 }
667 }
668
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']]);
675 }
676 }
677 }
678
679 if (empty($hasPrimary) && !empty($hasData)) {
680 $errors["{$name}[1][is_primary]"] = ts('One %1 should be marked as primary.', [1 => $label]);
681 }
682
683 if (count($hasPrimary) > 1) {
684 $errors["{$name}[" . array_pop($hasPrimary) . "][is_primary]"] = ts('Only one %1 can be marked as primary.',
685 [1 => $label]
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))) {
695 $errors["openid[$instance][openid]"] = ts('%1 already used.', [1 => $blocks['OpenID']]);
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 ) {
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;
716 }
717 }
718 }
719
720 if (!empty($invalidStreetNumbers)) {
721 $first = $invalidStreetNumbers[0];
722 foreach ($invalidStreetNumbers as & $num) {
723 $num = CRM_Contact_Form_Contact::ordinalNumber($num);
724 }
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)]);
726 }
727 }
728 }
729
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);
733 }
734
735 return $primaryID;
736 }
737
738 /**
739 * Build the form object.
740 */
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);
746 }
747
748 if ($this->_action == CRM_Core_Action::UPDATE) {
749 $deleteExtra = json_encode(ts('Are you sure you want to delete contact image.'));
750 $deleteURL = [
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;") . '"',
756 ],
757 ];
758 $deleteURL = CRM_Core_Action::formLink($deleteURL,
759 CRM_Core_Action::DELETE,
760 [
761 'id' => $this->_contactId,
762 ],
763 ts('more'),
764 FALSE,
765 'contact.image.delete',
766 'Contact',
767 $this->_contactId
768 );
769 $this->assign('deleteURL', $deleteURL);
770 }
771
772 //build contact type specific fields
773 $className = 'CRM_Contact_Form_Edit_' . $this->_contactType;
774 $className::buildQuickForm($this);
775
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));
782 }
783
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
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,
799 ]);
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 }
808 if ($name == 'TagsAndGroups') {
809 continue;
810 }
811 $className = 'CRM_Contact_Form_Edit_' . $name;
812 $className::buildQuickForm($this);
813 }
814
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');
818
819 // build location blocks.
820 CRM_Contact_Form_Edit_Lock::buildQuickForm($this);
821 CRM_Contact_Form_Location::buildQuickForm($this);
822
823 // add attachment
824 $this->addField('image_URL', ['maxlength' => '255', 'label' => ts('Browse/Upload Image')]);
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
840 $buttons = [
841 [
842 'type' => 'upload',
843 'name' => ts('Save'),
844 'subName' => 'view',
845 'isDefault' => TRUE,
846 ],
847 ];
848 if (CRM_Core_Permission::check('add contacts')) {
849 $buttons[] = [
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',
854 ];
855 }
856 $buttons[] = [
857 'type' => 'cancel',
858 'name' => ts('Cancel'),
859 ];
860
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)
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.
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);
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 }
887
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;
893 }
894 }
895
896 if (!empty($params['image_URL'])) {
897 CRM_Contact_BAO_Contact::processImageParams($params);
898 }
899
900 if (is_numeric(CRM_Utils_Array::value('current_employer_id', $params)) && !empty($params['current_employer'])) {
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) {
913 $params['contact_sub_type'] = [$this->_contactSubType];
914 }
915
916 if ($this->_contactId) {
917 $params['contact_id'] = $this->_contactId;
918 }
919
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;
924 }
925
926 if (isset($params['contact_id'])) {
927 // process membership status for deceased contact
928 $deceasedParams = [
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),
932 ];
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
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,
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
969 if (!array_key_exists('TagsAndGroups', $this->_editOptions)) {
970 unset($params['group']);
971 }
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;
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
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;
998 }
999 }
1000 }
1001 }
1002
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) {
1010 $message = ts('%1 has been updated.', [1 => $contact->display_name]);
1011 }
1012 else {
1013 $message = ts('%1 has been created.', [1 => $contact->display_name]);
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
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']);
1024 }
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
1042 $recentOther = [];
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')) {
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));
1069 }
1070 else {
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;
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 /**
1095 * Is there any real significant data in the hierarchical location array.
1096 *
1097 * @param array $fields
1098 * The hierarchical value representation of this location.
1099 *
1100 * @return bool
1101 * true if data exists, false otherwise
1102 */
1103 public static function blockDataExists(&$fields) {
1104 if (!is_array($fields)) {
1105 return FALSE;
1106 }
1107
1108 static $skipFields = [
1109 'location_type_id',
1110 'is_primary',
1111 'phone_type_id',
1112 'provider_id',
1113 'country_id',
1114 'website_type_id',
1115 'master_id',
1116 ];
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 /**
1147 * That checks for duplicate contacts.
1148 *
1149 * @param array $fields
1150 * Fields array which are submitted.
1151 * @param $errors
1152 * @param int $contactID
1153 * Contact id.
1154 * @param string $contactType
1155 * Contact type.
1156 */
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'])) {
1160
1161 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($fields, $contactType, 'Supervised', [$contactID]);
1162 if ($ids) {
1163
1164 $contactLinks = CRM_Contact_BAO_Contact_Utils::formatContactIDSToLinks($ids, TRUE, TRUE, $contactID);
1165
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 />',
1170 ]);
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',
1175 ]);
1176 }
1177 else {
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',
1181 ]);
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"> ';
1194 $row .= CRM_Utils_Array::value('display_name', $contactLinks['rows'][$i]);
1195 $row .= ' </td>';
1196 $row .= ' <td class="matching-contacts-email"> ';
1197 $row .= CRM_Utils_Array::value('primary_email', $contactLinks['rows'][$i]);
1198 $row .= ' </td>';
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]);
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
1212 // let smarty know that there are duplicates
1213 $template = CRM_Core_Smarty::singleton();
1214 $template->assign('isDuplicate', 1);
1215 }
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');
1219 }
1220 }
1221 }
1222
1223 /**
1224 * Use the form name to create the tpl file name.
1225 *
1226 * @return string
1227 */
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;
1234 }
1235 }
1236 return parent::getTemplateFileName();
1237 }
1238
1239 /**
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.
1244 *
1245 * @param array $params
1246 * of key value consist of address blocks.
1247 *
1248 * @return array
1249 * as array of success/fails for each address block
1250 */
1251 public function parseAddress(&$params) {
1252 $parseSuccess = $parsedFields = [];
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';
1262 foreach ([
1263 'street_number',
1264 'street_name',
1265 'street_unit',
1266 ] as $fld) {
1267 if (!empty($address[$fld])) {
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;
1294 foreach ([
1295 'street_number',
1296 'street_number_suffix',
1297 'street_name',
1298 'street_unit',
1299 ] as $fld) {
1300 if (in_array($fld, [
1301 'street_name',
1302 'street_unit',
1303 ])) {
1304 $streetAddress .= ' ';
1305 }
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 .= ' ';
1311 }
1312 }
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
1322 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
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 /**
1345 * Check parse result and if some address block fails then this
1346 * function return the status message for all address blocks.
1347 *
1348 * @param array $parseResult
1349 * An array of address blk instance and its status.
1350 *
1351 * @return null|string
1352 * $statusMsg string status message for all address blocks.
1353 */
1354 public static function parseAddressStatusMsg($parseResult) {
1355 $statusMsg = NULL;
1356 if (!is_array($parseResult) || empty($parseResult)) {
1357 return $statusMsg;
1358 }
1359
1360 $parseFails = [];
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.",
1369 [1 => implode(', ', $parseFails)]
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 *
1380 * @param int $number
1381 * number to convert in to ordinal number.
1382 *
1383 * @return string
1384 * ordinal number for given number.
1385 */
1386 public static function ordinalNumber($number) {
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 /**
1414 * Update membership status to deceased.
1415 * function return the status message for updated membership.
1416 *
1417 * @param array $deceasedParams
1418 * having contact id and deceased value.
1419 *
1420 * @return null|string
1421 * $updateMembershipMsg string status message for updated membership.
1422 */
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);
1427
1428 // process to set membership status to deceased for both active/inactive membership
1429 if ($contactId &&
1430 $this->_contactType == 'Individual' && !empty($deceasedParams['is_deceased'])
1431 ) {
1432
1433 $session = CRM_Core_Session::singleton();
1434 $userId = $session->get('userID');
1435 if (!$userId) {
1436 $userId = $contactId;
1437 }
1438
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');
1457 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
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
1466 $membershipLog = [
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,
1475 ];
1476
1477 CRM_Member_BAO_MembershipLog::add($membershipLog);
1478
1479 //create activity when membership status is changed
1480 $activityParam = [
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,
1493 ];
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.",
1502 [1 => $memCount]
1503 );
1504 }
1505 }
1506
1507 return $updateMembershipMsg;
1508 }
1509
1510 }