[REF] Further cleanup on address handling in merge code.
[civicrm-core.git] / CRM / Export / BAO / ExportProcessor.php
CommitLineData
6003a964 1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6003a964 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6003a964 9 +--------------------------------------------------------------------+
10 */
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6003a964 16 */
17
18/**
19 * Class CRM_Export_BAO_ExportProcessor
20 *
21 * Class to handle logic of export.
22 */
23class CRM_Export_BAO_ExportProcessor {
24
25 /**
26 * @var int
27 */
28 protected $queryMode;
29
30 /**
31 * @var int
32 */
33 protected $exportMode;
34
adabfa40 35 /**
36 * Array of fields in the main query.
37 *
38 * @var array
39 */
40 protected $queryFields = [];
41
71464b73 42 /**
43 * Either AND or OR.
44 *
45 * @var string
46 */
47 protected $queryOperator;
48
d41ab886 49 /**
50 * Requested output fields.
51 *
52 * If set to NULL then it is 'primary fields only'
53 * which actually means pretty close to all fields!
54 *
55 * @var array|null
56 */
57 protected $requestedFields;
58
b7db6051 59 /**
60 * Is the contact being merged into a single household.
61 *
62 * @var bool
63 */
64 protected $isMergeSameHousehold;
65
2761a916 66 /**
67 * Should contacts with the same address be merged.
68 *
69 * @var bool
70 */
71 protected $isMergeSameAddress = FALSE;
72
73 /**
74 * Fields that need to be retrieved for address merge purposes but should not be in output.
75 *
76 * @var array
77 */
78 protected $additionalFieldsForSameAddressMerge = [];
79
d97cefdc 80 /**
81 * Fields used for merging same contacts.
82 *
83 * @var array
84 */
85 protected $contactGreetingFields = [];
86
246e571e
JG
87 /**
88 * An array of primary IDs of the entity being exported.
89 *
90 * @var array
91 */
92 protected $ids = [];
93
d183e23a 94 /**
95 * Greeting options mapping to various greeting ids.
96 *
97 * This stores the option values for the addressee, postal_greeting & email_greeting
98 * option groups.
99 *
100 * @var array
101 */
102 protected $greetingOptions = [];
103
2761a916 104 /**
105 * Get additional non-visible fields for address merge purposes.
106 *
107 * @return array
108 */
109 public function getAdditionalFieldsForSameAddressMerge(): array {
110 return $this->additionalFieldsForSameAddressMerge;
111 }
112
113 /**
114 * Set additional non-visible fields for address merge purposes.
115 */
116 public function setAdditionalFieldsForSameAddressMerge() {
117 if ($this->isMergeSameAddress) {
118 $fields = ['id', 'master_id', 'state_province_id', 'postal_greeting_id', 'addressee_id'];
119 foreach ($fields as $index => $field) {
120 if (!empty($this->getReturnProperties()[$field])) {
121 unset($fields[$index]);
122 }
123 }
124 $this->additionalFieldsForSameAddressMerge = array_fill_keys($fields, 1);
125 }
126 }
127
128 /**
129 * Should contacts with the same address be merged.
130 *
131 * @return bool
132 */
133 public function isMergeSameAddress(): bool {
134 return $this->isMergeSameAddress;
135 }
136
137 /**
138 * Set same address is to be merged.
139 *
140 * @param bool $isMergeSameAddress
141 */
142 public function setIsMergeSameAddress(bool $isMergeSameAddress) {
143 $this->isMergeSameAddress = $isMergeSameAddress;
144 }
145
51d84eca 146 /**
147 * Additional fields required to export postal fields.
148 *
149 * @var array
150 */
151 protected $additionalFieldsForPostalExport = [];
152
153 /**
154 * Get additional fields required to do a postal export.
155 *
156 * @return array
157 */
158 public function getAdditionalFieldsForPostalExport() {
159 return $this->additionalFieldsForPostalExport;
160 }
161
162 /**
163 * Set additional fields required for a postal export.
164 */
165 public function setAdditionalFieldsForPostalExport() {
166 if ($this->getRequestedFields() && $this->isPostalableOnly()) {
167 $fields = ['is_deceased', 'do_not_mail', 'street_address', 'supplemental_address_1'];
168 foreach ($fields as $index => $field) {
169 if (!empty($this->getReturnProperties()[$field])) {
e8f0579a 170 unset($fields[$index]);
51d84eca 171 }
172 }
173 $this->additionalFieldsForPostalExport = array_fill_keys($fields, 1);
174 }
175 }
176
6d52bfe5 177 /**
178 * Only export contacts that can receive postal mail.
179 *
180 * Includes being alive, having an address & not having do_not_mail.
181 *
182 * @var bool
183 */
184 protected $isPostalableOnly;
185
944ed388 186 /**
187 * Key representing the head of household in the relationship array.
188 *
189 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
190 *
7b966967 191 * @var array
944ed388 192 */
193 protected $relationshipTypes = [];
194
704e3e9a 195 /**
196 * Array of properties to retrieve for relationships.
197 *
198 * @var array
199 */
200 protected $relationshipReturnProperties = [];
201
136f69a8 202 /**
203 * IDs of households that have already been exported.
204 *
205 * @var array
206 */
207 protected $exportedHouseholds = [];
208
ebebd629 209 /**
210 * Households to skip during export as they will be exported via their relationships anyway.
211 *
212 * @var array
213 */
214 protected $householdsToSkip = [];
215
e530cef4 216 /**
217 * Additional fields to return.
218 *
219 * This doesn't make much sense when we have a fields set but search build add it's own onto
220 * the 'Primary fields' (all) option.
221 *
222 * @var array
223 */
224 protected $additionalRequestedReturnProperties = [];
225
226 /**
227 * Get additional return properties.
228 *
229 * @return array
230 */
231 public function getAdditionalRequestedReturnProperties() {
232 return $this->additionalRequestedReturnProperties;
233 }
234
235 /**
236 * Set additional return properties.
237 *
238 * @param array $value
239 */
240 public function setAdditionalRequestedReturnProperties($value) {
241 // fix for CRM-7066
242 if (!empty($value['group'])) {
243 unset($value['group']);
244 $value['groups'] = 1;
245 }
246 $this->additionalRequestedReturnProperties = $value;
247 }
248
ce12a9e0 249 /**
250 * Get return properties by relationship.
251 * @return array
252 */
253 public function getRelationshipReturnProperties() {
254 return $this->relationshipReturnProperties;
255 }
256
257 /**
258 * Export values for related contacts.
259 *
260 * @var array
261 */
262 protected $relatedContactValues = [];
263
c66a5741 264 /**
265 * @var array
266 */
267 protected $returnProperties = [];
268
28254dcb 269 /**
270 * @var array
271 */
272 protected $outputSpecification = [];
273
e034ee91 274 /**
275 * @var string
276 */
277 protected $componentTable = '';
278
279 /**
280 * @return string
281 */
282 public function getComponentTable() {
283 return $this->componentTable;
284 }
285
286 /**
287 * Set the component table (if any).
288 *
289 * @param string $componentTable
290 */
291 public function setComponentTable($componentTable) {
292 $this->componentTable = $componentTable;
293 }
294
295 /**
296 * Clause from component search.
297 *
298 * @var string
299 */
300 protected $componentClause = '';
301
302 /**
303 * @return string
304 */
305 public function getComponentClause() {
306 return $this->componentClause;
307 }
308
309 /**
310 * @param string $componentClause
311 */
312 public function setComponentClause($componentClause) {
313 $this->componentClause = $componentClause;
314 }
315
68989e71 316 /**
317 * Name of a temporary table created to hold the results.
318 *
319 * Current decision making on when to create a temp table is kinda bad so this might change
320 * a bit as it is reviewed but basically we need a temp table or similar to calculate merging
321 * addresses. Merging households is handled in php. We create a temp table even when we don't need them.
322 *
323 * @var string
324 */
325 protected $temporaryTable;
326
327 /**
328 * @return string
329 */
330 public function getTemporaryTable(): string {
331 return $this->temporaryTable;
332 }
333
334 /**
335 * @param string $temporaryTable
336 */
337 public function setTemporaryTable(string $temporaryTable) {
338 $this->temporaryTable = $temporaryTable;
339 }
340
a2b21ae3 341 protected $postalGreetingTemplate;
342
343 /**
344 * @return mixed
345 */
346 public function getPostalGreetingTemplate() {
347 return $this->postalGreetingTemplate;
348 }
349
350 /**
351 * @param mixed $postalGreetingTemplate
352 */
353 public function setPostalGreetingTemplate($postalGreetingTemplate) {
354 $this->postalGreetingTemplate = $postalGreetingTemplate;
355 }
356
357 /**
358 * @return mixed
359 */
360 public function getAddresseeGreetingTemplate() {
361 return $this->addresseeGreetingTemplate;
362 }
363
364 /**
365 * @param mixed $addresseeGreetingTemplate
366 */
367 public function setAddresseeGreetingTemplate($addresseeGreetingTemplate) {
368 $this->addresseeGreetingTemplate = $addresseeGreetingTemplate;
369 }
370
371 protected $addresseeGreetingTemplate;
372
71464b73 373 /**
374 * CRM_Export_BAO_ExportProcessor constructor.
375 *
376 * @param int $exportMode
041ecc95 377 * @param array|null $requestedFields
71464b73 378 * @param string $queryOperator
b7db6051 379 * @param bool $isMergeSameHousehold
6d52bfe5 380 * @param bool $isPostalableOnly
2761a916 381 * @param bool $isMergeSameAddress
1e9e062d 382 * @param array $formValues
383 * Values from the export options form on contact export. We currently support these keys
384 * - postal_greeting
385 * - postal_other
386 * - addresee_greeting
387 * - addressee_other
388 */
389 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE, $isMergeSameAddress = FALSE, $formValues = []) {
71464b73 390 $this->setExportMode($exportMode);
391 $this->setQueryMode();
392 $this->setQueryOperator($queryOperator);
d41ab886 393 $this->setRequestedFields($requestedFields);
944ed388 394 $this->setRelationshipTypes();
b7db6051 395 $this->setIsMergeSameHousehold($isMergeSameHousehold);
806c5e1e 396 $this->setIsPostalableOnly($isPostalableOnly);
2761a916 397 $this->setIsMergeSameAddress($isMergeSameAddress);
806c5e1e 398 $this->setReturnProperties($this->determineReturnProperties());
2761a916 399 $this->setAdditionalFieldsForSameAddressMerge();
51d84eca 400 $this->setAdditionalFieldsForPostalExport();
3611a532 401 $this->setHouseholdMergeReturnProperties();
1e9e062d 402 $this->setGreetingStringsForSameAddressMerge($formValues);
d183e23a 403 $this->setGreetingOptions();
404 }
405
406 /**
407 * Set the greeting options, if relevant.
408 */
409 public function setGreetingOptions() {
410 if ($this->isMergeSameAddress()) {
411 $this->greetingOptions['addressee'] = CRM_Core_OptionGroup::values('addressee');
412 $this->greetingOptions['postal_greeting'] = CRM_Core_OptionGroup::values('postal_greeting');
413 }
d41ab886 414 }
415
6d52bfe5 416 /**
417 * @return bool
418 */
419 public function isPostalableOnly() {
420 return $this->isPostalableOnly;
421 }
422
423 /**
424 * @param bool $isPostalableOnly
425 */
426 public function setIsPostalableOnly($isPostalableOnly) {
427 $this->isPostalableOnly = $isPostalableOnly;
428 }
7b966967 429
d41ab886 430 /**
431 * @return array|null
432 */
433 public function getRequestedFields() {
04f8f758 434 return empty($this->requestedFields) ? NULL : $this->requestedFields;
d41ab886 435 }
436
437 /**
438 * @param array|null $requestedFields
439 */
440 public function setRequestedFields($requestedFields) {
441 $this->requestedFields = $requestedFields;
71464b73 442 }
443
c66a5741 444 /**
445 * @return array
446 */
447 public function getReturnProperties() {
51d84eca 448 return array_merge($this->returnProperties, $this->getAdditionalRequestedReturnProperties(), $this->getAdditionalFieldsForSameAddressMerge(), $this->getAdditionalFieldsForPostalExport());
c66a5741 449 }
450
451 /**
452 * @param array $returnProperties
453 */
454 public function setReturnProperties($returnProperties) {
455 $this->returnProperties = $returnProperties;
456 }
457
944ed388 458 /**
459 * @return array
460 */
461 public function getRelationshipTypes() {
462 return $this->relationshipTypes;
463 }
464
465 /**
466 */
467 public function setRelationshipTypes() {
468 $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
469 NULL,
470 NULL,
471 NULL,
472 NULL,
473 TRUE,
474 'name',
475 FALSE
476 );
477 }
478
ce12a9e0 479 /**
480 * Set the value for a relationship type field.
481 *
482 * In this case we are building up an array of properties for a related contact.
483 *
484 * These may be used for direct exporting or for merge to household depending on the
485 * options selected.
486 *
487 * @param string $relationshipType
488 * @param int $contactID
489 * @param string $field
490 * @param string $value
491 */
492 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
493 $this->relatedContactValues[$relationshipType][$contactID][$field] = $value;
ebebd629 494 if ($field === 'id') {
495 $this->householdsToSkip[] = $value;
496 }
ce12a9e0 497 }
498
499 /**
500 * Get the value for a relationship type field.
501 *
502 * In this case we are building up an array of properties for a related contact.
503 *
504 * These may be used for direct exporting or for merge to household depending on the
505 * options selected.
506 *
507 * @param string $relationshipType
508 * @param int $contactID
509 * @param string $field
510 *
511 * @return string
512 */
513 public function getRelationshipValue($relationshipType, $contactID, $field) {
514 return isset($this->relatedContactValues[$relationshipType][$contactID][$field]) ? $this->relatedContactValues[$relationshipType][$contactID][$field] : '';
515 }
516
136f69a8 517 /**
518 * Get the id of the related household.
519 *
520 * @param int $contactID
521 * @param string $relationshipType
522 *
523 * @return int
524 */
525 public function getRelatedHouseholdID($contactID, $relationshipType) {
526 return $this->relatedContactValues[$relationshipType][$contactID]['id'];
527 }
528
529 /**
530 * Has the household already been exported.
531 *
532 * @param int $housholdContactID
533 *
534 * @return bool
535 */
536 public function isHouseholdExported($housholdContactID) {
537 return isset($this->exportedHouseholds[$housholdContactID]);
538
539 }
540
b7db6051 541 /**
542 * @return bool
543 */
544 public function isMergeSameHousehold() {
545 return $this->isMergeSameHousehold;
546 }
547
548 /**
549 * @param bool $isMergeSameHousehold
550 */
551 public function setIsMergeSameHousehold($isMergeSameHousehold) {
552 $this->isMergeSameHousehold = $isMergeSameHousehold;
553 }
554
555 /**
556 * Return relationship types for household merge.
557 *
558 * @return mixed
559 */
560 public function getHouseholdRelationshipTypes() {
561 if (!$this->isMergeSameHousehold()) {
562 return [];
563 }
564 return [
565 CRM_Utils_Array::key('Household Member of', $this->getRelationshipTypes()),
566 CRM_Utils_Array::key('Head of Household for', $this->getRelationshipTypes()),
567 ];
568 }
944ed388 569
570 /**
571 * @param $fieldName
572 * @return bool
573 */
574 public function isRelationshipTypeKey($fieldName) {
575 return array_key_exists($fieldName, $this->relationshipTypes);
576 }
577
b7db6051 578 /**
579 * @param $fieldName
580 * @return bool
581 */
582 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
583 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
584 }
585
71464b73 586 /**
587 * @return string
588 */
589 public function getQueryOperator() {
590 return $this->queryOperator;
591 }
592
593 /**
594 * @param string $queryOperator
595 */
596 public function setQueryOperator($queryOperator) {
597 $this->queryOperator = $queryOperator;
598 }
599
246e571e
JG
600 /**
601 * @return array
602 */
603 public function getIds() {
604 return $this->ids;
605 }
606
607 /**
608 * @param array $ids
609 */
610 public function setIds($ids) {
611 $this->ids = $ids;
612 }
613
adabfa40 614 /**
615 * @return array
616 */
617 public function getQueryFields() {
749e60f4 618 return array_merge(
619 $this->queryFields,
620 $this->getComponentPaymentFields()
621 );
adabfa40 622 }
623
624 /**
625 * @param array $queryFields
626 */
627 public function setQueryFields($queryFields) {
5ebd7d09 628 // legacy hacks - we add these to queryFields because this
629 // pseudometadata is currently required.
630 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
631 $queryFields['country']['context'] = 'country';
632 $queryFields['world_region']['context'] = 'country';
633 $queryFields['state_province']['context'] = 'province';
808a80ba 634 $queryFields['contact_id'] = ['title' => ts('Contact ID'), 'type' => CRM_Utils_Type::T_INT];
adabfa40 635 $this->queryFields = $queryFields;
636 }
637
6003a964 638 /**
639 * @return int
640 */
641 public function getQueryMode() {
642 return $this->queryMode;
643 }
644
645 /**
646 * Set the query mode based on the export mode.
647 */
648 public function setQueryMode() {
649
650 switch ($this->getExportMode()) {
651 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
652 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE;
653 break;
654
655 case CRM_Export_Form_Select::EVENT_EXPORT:
656 $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT;
657 break;
658
659 case CRM_Export_Form_Select::MEMBER_EXPORT:
660 $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER;
661 break;
662
663 case CRM_Export_Form_Select::PLEDGE_EXPORT:
664 $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE;
665 break;
666
667 case CRM_Export_Form_Select::CASE_EXPORT:
668 $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE;
669 break;
670
671 case CRM_Export_Form_Select::GRANT_EXPORT:
672 $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT;
673 break;
674
675 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
676 $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY;
677 break;
678
679 default:
680 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS;
681 }
682 }
683
684 /**
685 * @return int
686 */
687 public function getExportMode() {
688 return $this->exportMode;
689 }
690
691 /**
692 * @param int $exportMode
693 */
694 public function setExportMode($exportMode) {
695 $this->exportMode = $exportMode;
696 }
697
29034a98 698 /**
699 * Get the name for the export file.
700 *
701 * @return string
702 */
703 public function getExportFileName() {
704 switch ($this->getExportMode()) {
705 case CRM_Export_Form_Select::CONTACT_EXPORT:
706 return ts('CiviCRM Contact Search');
707
708 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
709 return ts('CiviCRM Contribution Search');
710
711 case CRM_Export_Form_Select::MEMBER_EXPORT:
712 return ts('CiviCRM Member Search');
713
714 case CRM_Export_Form_Select::EVENT_EXPORT:
715 return ts('CiviCRM Participant Search');
716
717 case CRM_Export_Form_Select::PLEDGE_EXPORT:
718 return ts('CiviCRM Pledge Search');
719
720 case CRM_Export_Form_Select::CASE_EXPORT:
721 return ts('CiviCRM Case Search');
722
723 case CRM_Export_Form_Select::GRANT_EXPORT:
724 return ts('CiviCRM Grant Search');
725
726 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
727 return ts('CiviCRM Activity Search');
728
729 default:
730 // Legacy code suggests the value could be 'financial' - ie. something
731 // other than what should be accepted. However, I suspect that this line is
732 // never hit.
733 return ts('CiviCRM Search');
734 }
735 }
736
af17bedf 737 /**
738 * Get the label for the header row based on the field to output.
739 *
740 * @param string $field
741 *
742 * @return string
743 */
744 public function getHeaderForRow($field) {
745 if (substr($field, -11) == 'campaign_id') {
746 // @todo - set this correctly in the xml rather than here.
747 // This will require a generalised handling cleanup
748 return ts('Campaign ID');
749 }
750 if ($this->isMergeSameHousehold() && $field === 'id') {
751 return ts('Household ID');
752 }
753 elseif (isset($this->getQueryFields()[$field]['title'])) {
754 return $this->getQueryFields()[$field]['title'];
755 }
756 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
749e60f4 757 return CRM_Utils_Array::value($field, $this->getcomponentPaymentFields())['title'];
af17bedf 758 }
759 else {
760 return $field;
761 }
762 }
763
adabfa40 764 /**
765 * @param $params
766 * @param $order
6df77635 767 *
adabfa40 768 * @return array
769 */
6df77635 770 public function runQuery($params, $order) {
771 $returnProperties = $this->getReturnProperties();
6d52bfe5 772 $params = array_merge($params, $this->getWhereParams());
efaa3367 773
adabfa40 774 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
775 FALSE, FALSE, $this->getQueryMode(),
71464b73 776 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
adabfa40 777 );
778
779 //sort by state
780 //CRM-15301
781 $query->_sort = $order;
782 list($select, $from, $where, $having) = $query->query();
783 $this->setQueryFields($query->_fields);
e034ee91 784 $whereClauses = ['trash_clause' => "contact_a.is_deleted != 1"];
9858dc0b 785 if ($this->getRequestedFields() && ($this->getComponentTable())) {
e034ee91 786 $from .= " INNER JOIN " . $this->getComponentTable() . " ctTable ON ctTable.contact_id = contact_a.id ";
787 }
788 elseif ($this->getComponentClause()) {
789 $whereClauses[] = $this->getComponentClause();
790 }
791
792 // CRM-13982 - check if is deleted
793 foreach ($params as $value) {
794 if ($value[0] == 'contact_is_deleted') {
795 unset($whereClauses['trash_clause']);
796 }
797 }
798
efaa3367 799 if ($this->isPostalableOnly) {
800 if (array_key_exists('street_address', $returnProperties)) {
801 $addressWhere = " civicrm_address.street_address <> ''";
802 if (array_key_exists('supplemental_address_1', $returnProperties)) {
803 // We need this to be an OR rather than AND on the street_address so, hack it in.
804 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
805 'address_options', TRUE, NULL, TRUE
806 );
807 if (!empty($addressOptions['supplemental_address_1'])) {
808 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
809 }
810 }
62a39786 811 $whereClauses['address'] = '(' . $addressWhere . ')';
efaa3367 812 }
813 }
814
e034ee91 815 if (empty($where)) {
816 $where = "WHERE " . implode(' AND ', $whereClauses);
817 }
818 else {
819 $where .= " AND " . implode(' AND ', $whereClauses);
820 }
7628764a 821
822 $groupBy = $this->getGroupBy($query);
43767da2 823 $queryString = "$select $from $where $having $groupBy";
824 if ($order) {
825 // always add contact_a.id to the ORDER clause
826 // so the order is deterministic
827 //CRM-15301
828 if (strpos('contact_a.id', $order) === FALSE) {
829 $order .= ", contact_a.id";
830 }
831
832 list($field, $dir) = explode(' ', $order, 2);
833 $field = trim($field);
834 if (!empty($this->getReturnProperties()[$field])) {
835 //CRM-15301
836 $queryString .= " ORDER BY $order";
837 }
838 }
839 return [$query, $queryString];
adabfa40 840 }
841
28254dcb 842 /**
843 * Add a row to the specification for how to output data.
ae5d4caf 844 *
28254dcb 845 * @param string $key
28254dcb 846 * @param string $relationshipType
ae5d4caf 847 * @param string $locationType
848 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
28254dcb 849 */
ae5d4caf 850 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
5ebd7d09 851 $entityLabel = '';
ae5d4caf 852 if ($entityTypeID) {
853 if ($key === 'phone') {
5ebd7d09 854 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
ae5d4caf 855 }
856 if ($key === 'im') {
5ebd7d09 857 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
ae5d4caf 858 }
859 }
ae5d4caf 860
5ebd7d09 861 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
862 // but in time it may be good to rationalise them.
863 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
864 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
865 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
866
867 $this->outputSpecification[$index]['header'] = $label;
868 $this->outputSpecification[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
869
870 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
871 $this->setColumnAsCalculationOnly($index);
872 }
873 $this->outputSpecification[$index]['metadata'] = $this->getMetaDataForField($key);
874 }
875
876 /**
877 * Get the metadata for the given field.
878 *
879 * @param $key
880 *
881 * @return array
882 */
883 public function getMetaDataForField($key) {
884 $mappings = ['contact_id' => 'id'];
885 if (isset($this->getQueryFields()[$key])) {
886 return $this->getQueryFields()[$key];
887 }
888 if (isset($mappings[$key])) {
889 return $this->getQueryFields()[$mappings[$key]];
890 }
891 return [];
2cd3d767 892 }
ae5d4caf 893
2cd3d767 894 /**
895 * @param $key
896 */
897 public function setSqlColumnDefn($key) {
5ebd7d09 898 $this->outputSpecification[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
de6ff509 899 }
900
901 /**
902 * Mark a column as only required for calculations.
903 *
904 * Do not include the row with headers.
905 *
906 * @param string $column
907 */
908 public function setColumnAsCalculationOnly($column) {
909 $this->outputSpecification[$column]['do_not_output_to_csv'] = TRUE;
28254dcb 910 }
911
912 /**
913 * @return array
914 */
915 public function getHeaderRows() {
916 $headerRows = [];
917 foreach ($this->outputSpecification as $key => $spec) {
de6ff509 918 if (empty($spec['do_not_output_to_csv'])) {
919 $headerRows[] = $spec['header'];
920 }
28254dcb 921 }
922 return $headerRows;
923 }
924
2cd3d767 925 /**
926 * @return array
927 */
928 public function getSQLColumns() {
929 $sqlColumns = [];
930 foreach ($this->outputSpecification as $key => $spec) {
931 if (empty($spec['do_not_output_to_sql'])) {
932 $sqlColumns[$key] = $spec['sql_columns'];
933 }
934 }
935 return $sqlColumns;
936 }
937
5ebd7d09 938 /**
939 * @return array
940 */
941 public function getMetadata() {
942 $metadata = [];
943 foreach ($this->outputSpecification as $key => $spec) {
944 $metadata[$key] = $spec['metadata'];
945 }
946 return $metadata;
947 }
2cd3d767 948
6e2de55d 949 /**
950 * Build the row for output.
951 *
952 * @param \CRM_Contact_BAO_Query $query
953 * @param CRM_Core_DAO $iterationDAO
954 * @param array $outputColumns
955 * @param $metadata
956 * @param $paymentDetails
957 * @param $addPaymentHeader
6e2de55d 958 *
136f69a8 959 * @return array|bool
6e2de55d 960 */
c58085f8 961 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader) {
962 $paymentTableId = $this->getPaymentTableID();
ebebd629 963 if ($this->isHouseholdToSkip($iterationDAO->contact_id)) {
964 return FALSE;
965 }
6e2de55d 966 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
967 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
968
969 $row = [];
136f69a8 970 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id);
971 if ($householdMergeRelationshipType) {
972 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id, $householdMergeRelationshipType);
973 if ($this->isHouseholdExported($householdID)) {
974 return FALSE;
975 }
976 foreach (array_keys($outputColumns) as $column) {
977 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id, $column);
978 }
979 $this->markHouseholdExported($householdID);
980 return $row;
981 }
982
6e2de55d 983 $query->convertToPseudoNames($iterationDAO);
984
985 //first loop through output columns so that we return what is required, and in same order.
986 foreach ($outputColumns as $field => $value) {
6e2de55d 987 // add im_provider to $dao object
988 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
989 $iterationDAO->im_provider = $iterationDAO->provider_id;
990 }
991
992 //build row values (data)
993 $fieldValue = NULL;
994 if (property_exists($iterationDAO, $field)) {
995 $fieldValue = $iterationDAO->$field;
996 // to get phone type from phone type id
997 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
998 $fieldValue = $phoneTypes[$fieldValue];
999 }
1000 elseif ($field == 'provider_id' || $field == 'im_provider') {
1001 $fieldValue = CRM_Utils_Array::value($fieldValue, $imProviders);
1002 }
1003 elseif (strstr($field, 'master_id')) {
ffd511ee 1004 // @todo - why not just $field === 'master_id' - what else would it be?
1005 $masterAddressId = $iterationDAO->$field ?? NULL;
6e2de55d 1006 // get display name of contact that address is shared.
1007 $fieldValue = CRM_Contact_BAO_Contact::getMasterDisplayName($masterAddressId);
1008 }
1009 }
1010
1011 if ($this->isRelationshipTypeKey($field)) {
136f69a8 1012 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id, $value, $field);
6e2de55d 1013 }
1014 else {
1015 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
1016 }
1017 }
1018
1019 // If specific payment fields have been selected for export, payment
1020 // data will already be in $row. Otherwise, add payment related
1021 // information, if appropriate.
1022 if ($addPaymentHeader) {
1023 if (!$this->isExportSpecifiedPaymentFields()) {
1024 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
1025 if ($this->isExportPaymentFields()) {
1026 $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails);
1027 if (!is_array($paymentData) || empty($paymentData)) {
1028 $paymentData = $nullContributionDetails;
1029 }
1030 $row = array_merge($row, $paymentData);
1031 }
1032 elseif (!empty($paymentDetails)) {
1033 $row = array_merge($row, $nullContributionDetails);
1034 }
1035 }
1036 }
1037 //remove organization name for individuals if it is set for current employer
1038 if (!empty($row['contact_type']) &&
1039 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
1040 ) {
1041 $row['organization_name'] = '';
1042 }
1043 return $row;
1044 }
1045
136f69a8 1046 /**
1047 * If this row has a household whose details we should use get the relationship type key.
1048 *
1049 * @param $contactID
1050 *
1051 * @return bool
1052 */
1053 public function getHouseholdMergeTypeForRow($contactID) {
1054 if (!$this->isMergeSameHousehold()) {
1055 return FALSE;
1056 }
1057 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
1058 if (isset($this->relatedContactValues[$relationshipType][$contactID])) {
1059 return $relationshipType;
1060 }
1061 }
1062 }
1063
1064 /**
1065 * Mark the given household as already exported.
1066 *
1067 * @param $householdID
1068 */
1069 public function markHouseholdExported($householdID) {
1070 $this->exportedHouseholds[$householdID] = $householdID;
1071 }
1072
8bd67f47 1073 /**
1074 * @param $field
1075 * @param $iterationDAO
1076 * @param $fieldValue
1077 * @param $metadata
1078 * @param $paymentDetails
1079 *
1080 * @return string
1081 */
1082 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
1083
1084 $i18n = CRM_Core_I18n::singleton();
1085 if ($field == 'id') {
1086 return $iterationDAO->contact_id;
1087 // special case for calculated field
1088 }
1089 elseif ($field == 'source_contact_id') {
1090 return $iterationDAO->contact_id;
1091 }
1092 elseif ($field == 'pledge_balance_amount') {
1093 return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid;
1094 // special case for calculated field
1095 }
1096 elseif ($field == 'pledge_next_pay_amount') {
1097 return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount;
1098 }
1099 elseif (isset($fieldValue) &&
1100 $fieldValue != ''
1101 ) {
1102 //check for custom data
1103 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) {
1104 return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
1105 }
1106
be2fb01f 1107 elseif (in_array($field, [
8bd67f47 1108 'email_greeting',
1109 'postal_greeting',
1110 'addressee',
be2fb01f 1111 ])) {
8bd67f47 1112 //special case for greeting replacement
1113 $fldValue = "{$field}_display";
1114 return $iterationDAO->$fldValue;
1115 }
1116 else {
1117 //normal fields with a touch of CRM-3157
1118 switch ($field) {
1119 case 'country':
1120 case 'world_region':
be2fb01f 1121 return $i18n->crm_translate($fieldValue, ['context' => 'country']);
8bd67f47 1122
1123 case 'state_province':
be2fb01f 1124 return $i18n->crm_translate($fieldValue, ['context' => 'province']);
8bd67f47 1125
1126 case 'gender':
1127 case 'preferred_communication_method':
1128 case 'preferred_mail_format':
1129 case 'communication_style':
1130 return $i18n->crm_translate($fieldValue);
1131
1132 default:
1133 if (isset($metadata[$field])) {
1134 // No I don't know why we do it this way & whether we could
1135 // make better use of pseudoConstants.
1136 if (!empty($metadata[$field]['context'])) {
1137 return $i18n->crm_translate($fieldValue, $metadata[$field]);
1138 }
1139 if (!empty($metadata[$field]['pseudoconstant'])) {
001a515b 1140 if (!empty($metadata[$field]['bao'])) {
1141 return CRM_Core_PseudoConstant::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
1142 }
8bd67f47 1143 // This is not our normal syntax for pseudoconstants but I am a bit loath to
1144 // call an external function until sure it is not increasing php processing given this
1145 // may be iterated 100,000 times & we already have the $imProvider var loaded.
1146 // That can be next refactor...
1147 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
1148 // but I have to reach a stable point....
1149 $varName = $metadata[$field]['pseudoconstant']['var'];
1150 if ($varName === 'imProviders') {
1151 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
1152 }
1153 if ($varName === 'phoneTypes') {
1154 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
1155 }
1156 }
1157
1158 }
1159 return $fieldValue;
1160 }
1161 }
1162 }
1163 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
1164 $paymentTableId = $this->getPaymentTableID();
1165 $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails);
be2fb01f 1166 $payFieldMapper = [
8bd67f47 1167 'componentPaymentField_total_amount' => 'total_amount',
1168 'componentPaymentField_contribution_status' => 'contribution_status',
1169 'componentPaymentField_payment_instrument' => 'pay_instru',
1170 'componentPaymentField_transaction_id' => 'trxn_id',
1171 'componentPaymentField_received_date' => 'receive_date',
be2fb01f 1172 ];
8bd67f47 1173 return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, '');
1174 }
1175 else {
1176 // if field is empty or null
1177 return '';
1178 }
1179 }
1180
ce14544c 1181 /**
1182 * Get array of fields to return, over & above those defined in the main contact exportable fields.
1183 *
1184 * These include export mode specific fields & some fields apparently required as 'exportableFields'
1185 * but not returned by the function of the same name.
1186 *
1187 * @return array
1188 * Array of fields to return in the format ['field_name' => 1,...]
1189 */
1190 public function getAdditionalReturnProperties() {
ce14544c 1191 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) {
1192 $componentSpecificFields = [];
1193 }
1194 else {
1195 $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode());
1196 }
1197 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) {
1198 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode()));
d28b6cf2 1199 unset($componentSpecificFields['contribution_status_id']);
1200 unset($componentSpecificFields['pledge_status_id']);
1201 unset($componentSpecificFields['pledge_payment_status_id']);
ce14544c 1202 }
1203 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) {
1204 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode()));
1205 }
1206 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) {
1207 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE));
d28b6cf2 1208 unset($componentSpecificFields['contribution_status_id']);
ce14544c 1209 }
7626754f 1210 return $componentSpecificFields;
ce14544c 1211 }
1212
d41ab886 1213 /**
1214 * Should payment fields be appended to the export.
1215 *
1216 * (This is pretty hacky so hopefully this function won't last long - notice
1217 * how obviously it should be part of the above function!).
1218 */
1219 public function isExportPaymentFields() {
1220 if ($this->getRequestedFields() === NULL
f065c170 1221 && in_array($this->getQueryMode(), [
d41ab886 1222 CRM_Contact_BAO_Query::MODE_EVENT,
1223 CRM_Contact_BAO_Query::MODE_MEMBER,
1224 CRM_Contact_BAO_Query::MODE_PLEDGE,
1225 ])) {
1226 return TRUE;
1227 }
c66a5741 1228 elseif ($this->isExportSpecifiedPaymentFields()) {
1229 return TRUE;
1230 }
d41ab886 1231 return FALSE;
1232 }
1233
c66a5741 1234 /**
1235 * Has specific payment fields been requested (as opposed to via all fields).
1236 *
1237 * If specific fields have been requested then they get added at various points.
1238 *
1239 * @return bool
1240 */
1241 public function isExportSpecifiedPaymentFields() {
1242 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
1243 return TRUE;
1244 }
1245 }
1246
d41ab886 1247 /**
1248 * Get the name of the id field in the table that connects contributions to the export entity.
1249 */
1250 public function getPaymentTableID() {
1251 if ($this->getRequestedFields() === NULL) {
1252 $mapping = [
1253 CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id',
1254 CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id',
1255 CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id',
1256 ];
1257 return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : '';
1258 }
c66a5741 1259 elseif ($this->hasRequestedComponentPaymentFields()) {
1260 return 'participant_id';
1261 }
d41ab886 1262 return FALSE;
1263 }
1264
c66a5741 1265 /**
1266 * Have component payment fields been requested.
1267 *
1268 * @return bool
1269 */
1270 protected function hasRequestedComponentPaymentFields() {
1271 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) {
1272 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
1273 if (!empty($participantPaymentFields)) {
1274 return TRUE;
1275 }
1276 }
d41ab886 1277 return FALSE;
1278 }
1279
c66a5741 1280 /**
1281 * Get fields that indicate payment fields have been requested for a component.
1282 *
0e32ed68 1283 * Ideally this should be protected but making it temporarily public helps refactoring..
1284 *
c66a5741 1285 * @return array
1286 */
0e32ed68 1287 public function getComponentPaymentFields() {
c66a5741 1288 return [
749e60f4 1289 'componentPaymentField_total_amount' => ['title' => ts('Total Amount'), 'type' => CRM_Utils_Type::T_MONEY],
1290 'componentPaymentField_contribution_status' => ['title' => ts('Contribution Status'), 'type' => CRM_Utils_Type::T_STRING],
1291 'componentPaymentField_received_date' => ['title' => ts('Date Received'), 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME],
1292 'componentPaymentField_payment_instrument' => ['title' => ts('Payment Method'), 'type' => CRM_Utils_Type::T_STRING],
1293 'componentPaymentField_transaction_id' => ['title' => ts('Transaction ID'), 'type' => CRM_Utils_Type::T_STRING],
c66a5741 1294 ];
1295 }
1296
05ad310f 1297 /**
1298 * Get headers for payment fields.
1299 *
1300 * Returns an array of contribution fields when the entity supports payment fields and specific fields
1301 * are not specified. This is a transitional function for refactoring legacy code.
1302 */
1303 public function getPaymentHeaders() {
1304 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
749e60f4 1305 return CRM_Utils_Array::collect('title', $this->getcomponentPaymentFields());
05ad310f 1306 }
1307 return [];
1308 }
1309
d41ab886 1310 /**
1311 * Get the default properties when not specified.
1312 *
1313 * In the UI this appears as 'Primary fields only' but in practice it's
1314 * most of the kitchen sink and the hallway closet thrown in.
1315 *
1316 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
1317 *
1318 * @return array
1319 */
1320 public function getDefaultReturnProperties() {
1321 $returnProperties = [];
1322 $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE);
704e3e9a 1323 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [
1324 'groups',
1325 'tags',
7b966967 1326 'notes',
704e3e9a 1327 ];
d41ab886 1328
1329 foreach ($fields as $key => $var) {
1330 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1331 $returnProperties[$key] = 1;
1332 }
1333 }
1334 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1335 return $returnProperties;
1336 }
1337
704e3e9a 1338 /**
1339 * Add the field to relationship return properties & return it.
1340 *
1341 * This function is doing both setting & getting which is yuck but it is an interim
1342 * refactor.
1343 *
1344 * @param array $value
1345 * @param string $relationshipKey
1346 *
1347 * @return array
1348 */
1349 public function setRelationshipReturnProperties($value, $relationshipKey) {
04f8f758 1350 $relationField = $value['name'];
1351 $relIMProviderId = NULL;
1352 $relLocTypeId = CRM_Utils_Array::value('location_type_id', $value);
1353 $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
1354 $relPhoneTypeId = CRM_Utils_Array::value('phone_type_id', $value, ($locationName ? 'Primary' : NULL));
1355 $relIMProviderId = CRM_Utils_Array::value('im_provider_id', $value, ($locationName ? 'Primary' : NULL));
1356 if (in_array($relationField, $this->getValidLocationFields()) && $locationName) {
1357 if ($relationField === 'phone') {
a16a432a 1358 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
704e3e9a 1359 }
04f8f758 1360 elseif ($relationField === 'im') {
a16a432a 1361 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
704e3e9a 1362 }
1363 else {
a16a432a 1364 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1;
704e3e9a 1365 }
1366 }
1367 else {
1368 $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1;
1369 }
1370 return $this->relationshipReturnProperties[$relationshipKey];
1371 }
1372
ce12a9e0 1373 /**
1374 * Add the main return properties to the household merge properties if needed for merging.
1375 *
1376 * If we are using household merge we need to add these to the relationship properties to
1377 * be retrieved.
ce12a9e0 1378 */
6df77635 1379 public function setHouseholdMergeReturnProperties() {
3611a532 1380 if ($this->isMergeSameHousehold()) {
1381 $returnProperties = $this->getReturnProperties();
1382 $returnProperties = array_diff_key($returnProperties, array_fill_keys(['location_type', 'im_provider'], 1));
1383 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1384 $this->relationshipReturnProperties[$householdRelationshipType] = $returnProperties;
1385 }
ce12a9e0 1386 }
1387 }
1388
704e3e9a 1389 /**
1390 * Get the default location fields to request.
1391 *
1392 * @return array
1393 */
1394 public function getValidLocationFields() {
1395 return [
1396 'street_address',
1397 'supplemental_address_1',
1398 'supplemental_address_2',
1399 'supplemental_address_3',
1400 'city',
1401 'postal_code',
1402 'postal_code_suffix',
1403 'geo_code_1',
1404 'geo_code_2',
1405 'state_province',
1406 'country',
1407 'phone',
1408 'email',
1409 'im',
1410 ];
1411 }
1412
aa3a113b 1413 /**
c8adad81 1414 * Get the sql column definition for the given field.
1415 *
5ebd7d09 1416 * @param string $fieldName
1417 * @param string $columnName
aa3a113b 1418 *
1419 * @return mixed
1420 */
5ebd7d09 1421 public function getSqlColumnDefinition($fieldName, $columnName) {
aa3a113b 1422
1423 // early exit for master_id, CRM-12100
1424 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1425 // also for current_employer, CRM-16939
5ebd7d09 1426 if ($columnName == 'master_id' || $columnName == 'current_employer') {
aa3a113b 1427 return "$fieldName varchar(128)";
1428 }
1429
aa3a113b 1430 $queryFields = $this->getQueryFields();
714ab070 1431 // @todo remove the enotice avoidance here, ensure all columns are declared.
1432 // tests will fail on the enotices until they all are & then all the 'else'
1433 // below can go.
1434 $fieldSpec = $queryFields[$columnName] ?? [];
1435
aa3a113b 1436 // set the sql columns
714ab070 1437 if (isset($fieldSpec['type'])) {
1438 switch ($fieldSpec['type']) {
aa3a113b 1439 case CRM_Utils_Type::T_INT:
1440 case CRM_Utils_Type::T_BOOLEAN:
d989f2a4 1441 if (in_array(CRM_Utils_Array::value('data_type', $fieldSpec), ['Country', 'StateProvince', 'ContactReference'])) {
1442 return "$fieldName varchar(255)";
1443 }
714ab070 1444 return "$fieldName varchar(16)";
aa3a113b 1445
1446 case CRM_Utils_Type::T_STRING:
5ebd7d09 1447 if (isset($queryFields[$columnName]['maxlength'])) {
1448 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
aa3a113b 1449 }
1450 else {
1451 return "$fieldName varchar(255)";
1452 }
1453
1454 case CRM_Utils_Type::T_TEXT:
1455 case CRM_Utils_Type::T_LONGTEXT:
1456 case CRM_Utils_Type::T_BLOB:
1457 case CRM_Utils_Type::T_MEDIUMBLOB:
1458 return "$fieldName longtext";
1459
1460 case CRM_Utils_Type::T_FLOAT:
1461 case CRM_Utils_Type::T_ENUM:
1462 case CRM_Utils_Type::T_DATE:
1463 case CRM_Utils_Type::T_TIME:
1464 case CRM_Utils_Type::T_TIMESTAMP:
1465 case CRM_Utils_Type::T_MONEY:
1466 case CRM_Utils_Type::T_EMAIL:
1467 case CRM_Utils_Type::T_URL:
1468 case CRM_Utils_Type::T_CCNUM:
1469 default:
1470 return "$fieldName varchar(32)";
1471 }
1472 }
1473 else {
1474 if (substr($fieldName, -3, 3) == '_id') {
1475 return "$fieldName varchar(255)";
1476 }
1477 elseif (substr($fieldName, -5, 5) == '_note') {
1478 return "$fieldName text";
1479 }
1480 else {
1481 $changeFields = [
1482 'groups',
1483 'tags',
1484 'notes',
1485 ];
1486
1487 if (in_array($fieldName, $changeFields)) {
1488 return "$fieldName text";
1489 }
1490 else {
1491 // set the sql columns for custom data
5ebd7d09 1492 if (isset($queryFields[$columnName]['data_type'])) {
aa3a113b 1493
5ebd7d09 1494 switch ($queryFields[$columnName]['data_type']) {
aa3a113b 1495 case 'String':
1496 // May be option labels, which could be up to 512 characters
5ebd7d09 1497 $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$columnName]));
aa3a113b 1498 return "$fieldName varchar($length)";
1499
aa3a113b 1500 case 'Link':
1501 return "$fieldName varchar(255)";
1502
1503 case 'Memo':
1504 return "$fieldName text";
1505
1506 default:
1507 return "$fieldName varchar(255)";
1508 }
1509 }
1510 else {
1511 return "$fieldName text";
1512 }
1513 }
1514 }
1515 }
1516 }
1517
c8adad81 1518 /**
1519 * Get the munged field name.
1520 *
1521 * @param string $field
1522 * @return string
1523 */
1524 public function getMungedFieldName($field) {
1525 $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64);
1526 if ($fieldName == 'id') {
1527 $fieldName = 'civicrm_primary_id';
1528 }
1529 return $fieldName;
1530 }
1531
5ebd7d09 1532 /**
1533 * In order to respect the history of this class we need to index kinda illogically.
1534 *
1535 * On the bright side - this stuff is tested within a nano-byte of it's life.
1536 *
1537 * e.g '2-a-b_Home-City'
1538 *
1539 * @param string $key
1540 * @param string $relationshipType
1541 * @param string $locationType
1542 * @param $entityLabel
1543 *
1544 * @return string
1545 */
1546 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1547 if ($entityLabel || $key === 'im') {
5ebd7d09 1548 // Just cos that's the history...
1549 if ($key !== 'master_id') {
1550 $key = $this->getHeaderForRow($key);
1551 }
1552 }
1553 if (!$relationshipType || $key !== 'id') {
1554 $key = $this->getMungedFieldName($key);
1555 }
1556 return $this->getMungedFieldName(
1557 ($relationshipType ? ($relationshipType . '_') : '')
1558 . ($locationType ? ($locationType . '_') : '')
1559 . $key
1560 . ($entityLabel ? ('_' . $entityLabel) : '')
1561 );
1562 }
1563
1564 /**
1565 * Get the compiled label for the column.
1566 *
1567 * e.g 'Gender', 'Employee Of-Home-city'
1568 *
1569 * @param string $key
1570 * @param string $relationshipType
1571 * @param string $locationType
1572 * @param string $entityLabel
1573 *
1574 * @return string
1575 */
1576 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1577 return ($relationshipType ? $this->getRelationshipTypes()[$relationshipType] . '-' : '')
1578 . ($locationType ? $locationType . '-' : '')
1579 . $this->getHeaderForRow($key)
1580 . ($entityLabel ? '-' . $entityLabel : '');
1581 }
1582
1583 /**
1584 * Get the mysql field name key.
1585 *
1586 * This key is locked in by tests but the reasons for the specific conventions -
1587 * ie. headings are used for keying fields in some cases, are likely
1588 * accidental rather than deliberate.
1589 *
1590 * This key is used for the output sql array.
1591 *
1592 * @param string $key
1593 * @param $relationshipType
1594 * @param $locationType
1595 * @param $entityLabel
1596 *
1597 * @return string
1598 */
1599 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1600 if ($entityLabel || $key === 'im') {
5ebd7d09 1601 if ($key !== 'state_province' && $key !== 'id') {
001a515b 1602 // @todo - test removing this - indexing by $key should be fine...
5ebd7d09 1603 $key = $this->getHeaderForRow($key);
1604 }
1605 }
1606 if (!$relationshipType || $key !== 'id') {
1607 $key = $this->getMungedFieldName($key);
1608 }
1609 $fieldKey = $this->getMungedFieldName(
1610 ($relationshipType ? ($relationshipType . '_') : '')
1611 . ($locationType ? ($locationType . '_') : '')
1612 . $key
1613 . ($entityLabel ? ('_' . $entityLabel) : '')
1614 );
1615 return $fieldKey;
1616 }
1617
6d52bfe5 1618 /**
1619 * Get params for the where criteria.
1620 *
1621 * @return mixed
1622 */
1623 public function getWhereParams() {
1624 if (!$this->isPostalableOnly()) {
1625 return [];
1626 }
1627 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1628 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1629 return $params;
1630 }
1631
136f69a8 1632 /**
1633 * @param $row
1634 * @param $contactID
1635 * @param $value
1636 * @param $field
1637 */
1638 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1639 foreach (array_keys($value) as $property) {
1640 if ($property === 'location') {
1641 // @todo just undo all this nasty location wrangling!
1642 foreach ($value['location'] as $locationKey => $locationFields) {
1643 foreach (array_keys($locationFields) as $locationField) {
1644 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1645 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1646 }
1647 }
1648 }
1649 else {
1650 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1651 }
1652 }
1653 }
1654
ebebd629 1655 /**
1656 * Is this contact a household that is already set to be exported by virtue of it's household members.
1657 *
1658 * @param int $contactID
1659 *
1660 * @return bool
1661 */
1662 protected function isHouseholdToSkip($contactID) {
1663 return in_array($contactID, $this->householdsToSkip);
1664 }
1665
44fc53ab 1666 /**
1667 * Get the various arrays that we use to structure our output.
1668 *
1669 * The extraction of these has been moved to a separate function for clarity and so that
1670 * tests can be added - in particular on the $outputHeaders array.
1671 *
1672 * However it still feels a bit like something that I'm too polite to write down and this should be seen
1673 * as a step on the refactoring path rather than how it should be.
1674 *
1675 * @return array
1676 * - outputColumns Array of columns to be exported. The values don't matter but the key must match the
1677 * alias for the field generated by BAO_Query object.
1678 * - headerRows Array of the column header strings to put in the csv header - non-associative.
1679 * - sqlColumns Array of column names for the temp table. Not too sure why outputColumns can't be used here.
1680 * - metadata Array of fields with specific parameters to pass to the translate function or another hacky nasty solution
1681 * I'm too embarassed to discuss here.
1682 * The keys need
1683 * - to match the outputColumns keys (yes, the fact we ignore the output columns values & then pass another array with values
1684 * we could use does suggest further refactors. However, you future improver, do remember that every check you do
1685 * in the main DAO loop is done once per row & that coule be 100,000 times.)
1686 * Finally a pop quiz: We need the translate context because we use a function other than ts() - is this because
1687 * - a) the function used is more efficient or
1688 * - b) this code is old & outdated. Submit your answers to circular bin or better
1689 * yet find a way to comment them for posterity.
1690 */
1691 public function getExportStructureArrays() {
1692 $outputColumns = $metadata = [];
1693 $queryFields = $this->getQueryFields();
1694 foreach ($this->getReturnProperties() as $key => $value) {
1695 if (($key != 'location' || !is_array($value)) && !$this->isRelationshipTypeKey($key)) {
1696 $outputColumns[$key] = $value;
1697 $this->addOutputSpecification($key);
1698 }
1699 elseif ($this->isRelationshipTypeKey($key)) {
1700 $outputColumns[$key] = $value;
1701 foreach ($value as $relationField => $relationValue) {
1702 // below block is same as primary block (duplicate)
1703 if (isset($queryFields[$relationField]['title'])) {
1704 $this->addOutputSpecification($relationField, $key);
1705 }
1706 elseif (is_array($relationValue) && $relationField == 'location') {
1707 // fix header for location type case
1708 foreach ($relationValue as $ltype => $val) {
1709 foreach (array_keys($val) as $fld) {
1710 $type = explode('-', $fld);
1711 $this->addOutputSpecification($type[0], $key, $ltype, CRM_Utils_Array::value(1, $type));
1712 }
1713 }
1714 }
1715 }
1716 }
1717 else {
1718 foreach ($value as $locationType => $locationFields) {
1719 foreach (array_keys($locationFields) as $locationFieldName) {
1720 $type = explode('-', $locationFieldName);
1721
1722 $actualDBFieldName = $type[0];
1723 $daoFieldName = CRM_Utils_String::munge($locationType) . '-' . $actualDBFieldName;
1724
1725 if (!empty($type[1])) {
1726 $daoFieldName .= "-" . $type[1];
1727 }
1728 $this->addOutputSpecification($actualDBFieldName, NULL, $locationType, CRM_Utils_Array::value(1, $type));
1729 $metadata[$daoFieldName] = $this->getMetaDataForField($actualDBFieldName);
1730 $outputColumns[$daoFieldName] = TRUE;
1731 }
1732 }
1733 }
1734 }
1735 return [$outputColumns, $metadata];
1736 }
1737
ccc3ac8a 1738 /**
1739 * Get default return property for export based on mode
1740 *
1741 * @return string
1742 * Default Return property
1743 */
1744 public function defaultReturnProperty() {
1745 // hack to add default return property based on export mode
1746 $property = NULL;
1747 $exportMode = $this->getExportMode();
1748 if ($exportMode == CRM_Export_Form_Select::CONTRIBUTE_EXPORT) {
1749 $property = 'contribution_id';
1750 }
1751 elseif ($exportMode == CRM_Export_Form_Select::EVENT_EXPORT) {
1752 $property = 'participant_id';
1753 }
1754 elseif ($exportMode == CRM_Export_Form_Select::MEMBER_EXPORT) {
1755 $property = 'membership_id';
1756 }
1757 elseif ($exportMode == CRM_Export_Form_Select::PLEDGE_EXPORT) {
1758 $property = 'pledge_id';
1759 }
1760 elseif ($exportMode == CRM_Export_Form_Select::CASE_EXPORT) {
1761 $property = 'case_id';
1762 }
1763 elseif ($exportMode == CRM_Export_Form_Select::GRANT_EXPORT) {
1764 $property = 'grant_id';
1765 }
1766 elseif ($exportMode == CRM_Export_Form_Select::ACTIVITY_EXPORT) {
1767 $property = 'activity_id';
1768 }
1769 return $property;
1770 }
1771
806c5e1e 1772 /**
1773 * Determine the required return properties from the input parameters.
1774 *
1775 * @return array
1776 */
1777 public function determineReturnProperties() {
1778 if ($this->getRequestedFields()) {
1779 $returnProperties = [];
1780 foreach ($this->getRequestedFields() as $key => $value) {
04f8f758 1781 $fieldName = $value['name'];
7d0e773c 1782 $locationName = !empty($value['location_type_id']) ? CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $value['location_type_id']) : NULL;
04f8f758 1783 $relationshipTypeKey = !empty($value['relationship_type_id']) ? $value['relationship_type_id'] . '_' . $value['relationship_direction'] : NULL;
1784 if (!$fieldName || $this->isHouseholdMergeRelationshipTypeKey($relationshipTypeKey)) {
806c5e1e 1785 continue;
1786 }
1787
04f8f758 1788 if ($this->isRelationshipTypeKey($relationshipTypeKey)) {
1789 $returnProperties[$relationshipTypeKey] = $this->setRelationshipReturnProperties($value, $relationshipTypeKey);
806c5e1e 1790 }
04f8f758 1791 elseif ($locationName) {
1792 if ($fieldName === 'phone') {
1793 $returnProperties['location'][$locationName]['phone-' . $value['phone_type_id'] ?? NULL] = 1;
806c5e1e 1794 }
04f8f758 1795 elseif ($fieldName === 'im') {
1796 $returnProperties['location'][$locationName]['im-' . $value['im_provider_id'] ?? NULL] = 1;
806c5e1e 1797 }
1798 else {
1799 $returnProperties['location'][$locationName][$fieldName] = 1;
1800 }
1801 }
1802 else {
1803 //hack to fix component fields
1804 //revert mix of event_id and title
1805 if ($fieldName == 'event_id') {
1806 $returnProperties['event_id'] = 1;
1807 }
1808 else {
1809 $returnProperties[$fieldName] = 1;
1810 }
1811 }
1812 }
1813 $defaultExportMode = $this->defaultReturnProperty();
1814 if ($defaultExportMode) {
1815 $returnProperties[$defaultExportMode] = 1;
1816 }
1817 }
1818 else {
1819 $returnProperties = $this->getDefaultReturnProperties();
1820 }
25e51a1c 1821 if ($this->isMergeSameHousehold()) {
1822 $returnProperties['id'] = 1;
1823 }
2761a916 1824 if ($this->isMergeSameAddress()) {
1825 $returnProperties['addressee'] = 1;
1826 $returnProperties['postal_greeting'] = 1;
1827 $returnProperties['email_greeting'] = 1;
1828 $returnProperties['street_name'] = 1;
1829 $returnProperties['household_name'] = 1;
1830 $returnProperties['street_address'] = 1;
1831 $returnProperties['city'] = 1;
1832 $returnProperties['state_province'] = 1;
1833
1834 }
806c5e1e 1835 return $returnProperties;
1836 }
1837
571f3b5b 1838 /**
7628764a 1839 * @param object $query
1840 * CRM_Contact_BAO_Query
1841 *
1842 * @return string
1843 * Group By Clause
1844 */
1845 public function getGroupBy($query) {
1846 $groupBy = NULL;
1847 $returnProperties = $this->getReturnProperties();
1848 $exportMode = $this->getExportMode();
1849 $queryMode = $this->getQueryMode();
1850 if (!empty($returnProperties['tags']) || !empty($returnProperties['groups']) ||
1851 CRM_Utils_Array::value('notes', $returnProperties) ||
1852 // CRM-9552
1853 ($queryMode & CRM_Contact_BAO_Query::MODE_CONTACTS && $query->_useGroupBy)
1854 ) {
1855 $groupBy = "contact_a.id";
1856 }
1857
1858 switch ($exportMode) {
1859 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
1860 $groupBy = 'civicrm_contribution.id';
1861 if (CRM_Contribute_BAO_Query::isSoftCreditOptionEnabled()) {
1862 // especial group by when soft credit columns are included
1863 $groupBy = ['contribution_search_scredit_combined.id', 'contribution_search_scredit_combined.scredit_id'];
1864 }
1865 break;
1866
1867 case CRM_Export_Form_Select::EVENT_EXPORT:
1868 $groupBy = 'civicrm_participant.id';
1869 break;
1870
1871 case CRM_Export_Form_Select::MEMBER_EXPORT:
1872 $groupBy = "civicrm_membership.id";
1873 break;
1874 }
1875
1876 if ($queryMode & CRM_Contact_BAO_Query::MODE_ACTIVITY) {
1877 $groupBy = "civicrm_activity.id ";
1878 }
1879
1880 return $groupBy ? ' GROUP BY ' . implode(', ', (array) $groupBy) : '';
1881 }
806c5e1e 1882
571f3b5b 1883 /**
1884 * @param int $contactId
571f3b5b 1885 *
1886 * @return array
1887 */
423e3da6 1888 public function replaceMergeTokens($contactId) {
571f3b5b 1889 $greetings = [];
1890 $contact = NULL;
1891
1892 $greetingFields = [
a2b21ae3 1893 'postal_greeting' => $this->getPostalGreetingTemplate(),
1894 'addressee' => $this->getAddresseeGreetingTemplate(),
571f3b5b 1895 ];
a2b21ae3 1896 foreach ($greetingFields as $greeting => $greetingLabel) {
423e3da6 1897 $tokens = CRM_Utils_Token::getTokens($greetingLabel);
1898 if (!empty($tokens)) {
571f3b5b 1899 if (empty($contact)) {
1900 $values = [
1901 'id' => $contactId,
1902 'version' => 3,
1903 ];
1904 $contact = civicrm_api('contact', 'get', $values);
1905
1906 if (!empty($contact['is_error'])) {
1907 return $greetings;
1908 }
1909 $contact = $contact['values'][$contact['id']];
1910 }
1911
1912 $tokens = ['contact' => $greetingLabel];
1913 $greetings[$greeting] = CRM_Utils_Token::replaceContactTokens($greetingLabel, $contact, NULL, $tokens);
1914 }
1915 }
1916 return $greetings;
1917 }
1918
c8713fd4 1919 /**
1920 * Build array for merging same addresses.
1921 *
1922 * @param $sql
c8713fd4 1923 * @param bool $sharedAddress
1924 *
1925 * @return array
1926 */
ee769c00 1927 public function buildMasterCopyArray($sql, $sharedAddress = FALSE) {
c8713fd4 1928
c8713fd4 1929 $merge = $parents = [];
1930 $dao = CRM_Core_DAO::executeQuery($sql);
1931
1932 while ($dao->fetch()) {
1933 $masterID = $dao->master_id;
1934 $copyID = $dao->copy_id;
c8713fd4 1935
cf7c5cb8 1936 $this->cacheContactGreetings((int) $dao->master_contact_id);
1937 $this->cacheContactGreetings((int) $dao->copy_contact_id);
c8713fd4 1938
1939 if (!isset($merge[$masterID])) {
1940 // check if this is an intermediate child
1941 // this happens if there are 3 or more matches a,b, c
1942 // the above query will return a, b / a, c / b, c
1943 // we might be doing a bit more work, but for now its ok, unless someone
1944 // knows how to fix the query above
1945 if (isset($parents[$masterID])) {
1946 $masterID = $parents[$masterID];
1947 }
1948 else {
1949 $merge[$masterID] = [
d183e23a 1950 'addressee' => $this->getContactGreeting((int) $dao->master_contact_id, 'addressee', $dao->master_addressee),
c8713fd4 1951 'copy' => [],
d183e23a 1952 'postalGreeting' => $this->getContactGreeting((int) $dao->master_contact_id, 'postal_greeting', $dao->master_postal_greeting),
c8713fd4 1953 ];
1954 $merge[$masterID]['emailGreeting'] = &$merge[$masterID]['postalGreeting'];
1955 }
1956 }
1957 $parents[$copyID] = $masterID;
1958
1959 if (!$sharedAddress && !array_key_exists($copyID, $merge[$masterID]['copy'])) {
d183e23a 1960 $copyPostalGreeting = $this->getContactPortionOfGreeting((int) $dao->copy_contact_id, (int) $dao->copy_postal_greeting_id, 'postal_greeting', $dao->copy_postal_greeting);
ee769c00 1961 if ($copyPostalGreeting) {
c8713fd4 1962 $merge[$masterID]['postalGreeting'] = "{$merge[$masterID]['postalGreeting']}, {$copyPostalGreeting}";
1963 // if there happens to be a duplicate, remove it
1964 $merge[$masterID]['postalGreeting'] = str_replace(" {$copyPostalGreeting},", "", $merge[$masterID]['postalGreeting']);
1965 }
1966
d183e23a 1967 $copyAddressee = $this->getContactPortionOfGreeting((int) $dao->copy_contact_id, (int) $dao->copy_addressee_id, 'addressee', $dao->copy_addressee);
ee769c00 1968 if ($copyAddressee) {
c8713fd4 1969 $merge[$masterID]['addressee'] = "{$merge[$masterID]['addressee']}, " . trim($copyAddressee);
1970 }
1971 }
d183e23a 1972 $merge[$masterID]['copy'][$copyID] = $copyAddressee ?? $dao->copy_addressee;
c8713fd4 1973 }
1974
1975 return $merge;
1976 }
1977
30237bf3 1978 /**
1979 * Merge contacts with the same address.
30237bf3 1980 */
f8fb0608 1981 public function mergeSameAddress() {
a2b21ae3 1982
30237bf3 1983 $tableName = $this->getTemporaryTable();
1984 // check if any records are present based on if they have used shared address feature,
1985 // and not based on if city / state .. matches.
1986 $sql = "
1987SELECT r1.id as copy_id,
1988 r1.civicrm_primary_id as copy_contact_id,
1989 r1.addressee as copy_addressee,
1990 r1.addressee_id as copy_addressee_id,
1991 r1.postal_greeting as copy_postal_greeting,
1992 r1.postal_greeting_id as copy_postal_greeting_id,
1993 r2.id as master_id,
1994 r2.civicrm_primary_id as master_contact_id,
1995 r2.postal_greeting as master_postal_greeting,
1996 r2.postal_greeting_id as master_postal_greeting_id,
1997 r2.addressee as master_addressee,
1998 r2.addressee_id as master_addressee_id
1999FROM $tableName r1
2000INNER JOIN civicrm_address adr ON r1.master_id = adr.id
2001INNER JOIN $tableName r2 ON adr.contact_id = r2.civicrm_primary_id
2002ORDER BY r1.id";
ee769c00 2003 $linkedMerge = $this->buildMasterCopyArray($sql, TRUE);
30237bf3 2004
2005 // find all the records that have the same street address BUT not in a household
2006 // require match on city and state as well
2007 $sql = "
2008SELECT r1.id as master_id,
2009 r1.civicrm_primary_id as master_contact_id,
2010 r1.postal_greeting as master_postal_greeting,
2011 r1.postal_greeting_id as master_postal_greeting_id,
2012 r1.addressee as master_addressee,
2013 r1.addressee_id as master_addressee_id,
2014 r2.id as copy_id,
2015 r2.civicrm_primary_id as copy_contact_id,
2016 r2.postal_greeting as copy_postal_greeting,
2017 r2.postal_greeting_id as copy_postal_greeting_id,
2018 r2.addressee as copy_addressee,
2019 r2.addressee_id as copy_addressee_id
2020FROM $tableName r1
2021LEFT JOIN $tableName r2 ON ( r1.street_address = r2.street_address AND
2022 r1.city = r2.city AND
2023 r1.state_province_id = r2.state_province_id )
2024WHERE ( r1.household_name IS NULL OR r1.household_name = '' )
2025AND ( r2.household_name IS NULL OR r2.household_name = '' )
2026AND ( r1.street_address != '' )
2027AND r2.id > r1.id
2028ORDER BY r1.id
2029";
ee769c00 2030 $merge = $this->buildMasterCopyArray($sql);
30237bf3 2031
2032 // unset ids from $merge already present in $linkedMerge
2033 foreach ($linkedMerge as $masterID => $values) {
2034 $keys = [$masterID];
2035 $keys = array_merge($keys, array_keys($values['copy']));
2036 foreach ($merge as $mid => $vals) {
2037 if (in_array($mid, $keys)) {
2038 unset($merge[$mid]);
2039 }
2040 else {
2041 foreach ($values['copy'] as $copyId) {
2042 if (in_array($copyId, $keys)) {
2043 unset($merge[$mid]['copy'][$copyId]);
2044 }
2045 }
2046 }
2047 }
2048 }
2049 $merge = $merge + $linkedMerge;
2050
2051 foreach ($merge as $masterID => $values) {
2052 $sql = "
2053UPDATE $tableName
2054SET addressee = %1, postal_greeting = %2, email_greeting = %3
2055WHERE id = %4
2056";
2057 $params = [
2058 1 => [$values['addressee'], 'String'],
2059 2 => [$values['postalGreeting'], 'String'],
2060 3 => [$values['emailGreeting'], 'String'],
2061 4 => [$masterID, 'Integer'],
2062 ];
2063 CRM_Core_DAO::executeQuery($sql, $params);
2064
2065 // delete all copies
2066 $deleteIDs = array_keys($values['copy']);
2067 $deleteIDString = implode(',', $deleteIDs);
2068 $sql = "
2069DELETE FROM $tableName
2070WHERE id IN ( $deleteIDString )
2071";
2072 CRM_Core_DAO::executeQuery($sql);
2073 }
30237bf3 2074 }
2075
e358ebd1 2076 /**
2077 * The function unsets static part of the string, if token is the dynamic part.
2078 *
2079 * Example: 'Hello {contact.first_name}' => converted to => '{contact.first_name}'
2080 * i.e 'Hello Alan' => converted to => 'Alan'
2081 *
2082 * @param string $parsedString
2083 * @param string $defaultGreeting
ca76de35 2084 * @param string $greetingLabel
e358ebd1 2085 *
2086 * @return mixed
2087 */
2088 public function trimNonTokensFromAddressString(
2089 &$parsedString, $defaultGreeting,
ca76de35 2090 $greetingLabel
e358ebd1 2091 ) {
e358ebd1 2092 $greetingLabel = empty($greetingLabel) ? $defaultGreeting : $greetingLabel;
2093
2094 $stringsToBeReplaced = preg_replace('/(\{[a-zA-Z._ ]+\})/', ';;', $greetingLabel);
2095 $stringsToBeReplaced = explode(';;', $stringsToBeReplaced);
2096 foreach ($stringsToBeReplaced as $key => $string) {
2097 // to keep one space
2098 $stringsToBeReplaced[$key] = ltrim($string);
2099 }
2100 $parsedString = str_replace($stringsToBeReplaced, "", $parsedString);
2101
2102 return $parsedString;
2103 }
2104
978f5005 2105 /**
2106 * Preview export output.
2107 *
2108 * @param int $limit
2109 * @return array
2110 */
2111 public function getPreview($limit) {
2112 $rows = [];
2113 list($outputColumns, $metadata) = $this->getExportStructureArrays();
2114 $query = $this->runQuery([], '');
2115 CRM_Core_DAO::disableFullGroupByMode();
2116 $result = CRM_Core_DAO::executeQuery($query[1] . ' LIMIT ' . (int) $limit);
2117 CRM_Core_DAO::reenableFullGroupByMode();
2118 while ($result->fetch()) {
2119 $rows[] = $this->buildRow($query[0], $result, $outputColumns, $metadata, [], []);
2120 }
2121 return $rows;
2122 }
2123
1e9e062d 2124 /**
2125 * Set the template strings to be used when merging two contacts with the same address.
2126 *
2127 * @param array $formValues
2128 * Values from first form. In this case we care about the keys
2129 * - postal_greeting
2130 * - postal_other
2131 * - address_greeting
2132 * - addressee_other
2133 *
2134 * @return mixed
2135 */
2136 protected function setGreetingStringsForSameAddressMerge($formValues) {
2137 $greetingOptions = CRM_Export_Form_Select::getGreetingOptions();
2138
2139 if (!empty($greetingOptions)) {
2140 // Greeting options is keyed by 'postal_greeting' or 'addressee'.
2141 foreach ($greetingOptions as $key => $value) {
2142 $option = CRM_Utils_Array::value($key, $formValues);
2143 if ($option) {
2144 if ($greetingOptions[$key][$option] == ts('Other')) {
2145 $formValues[$key] = $formValues["{$key}_other"];
2146 }
2147 elseif ($greetingOptions[$key][$option] == ts('List of names')) {
2148 $formValues[$key] = '';
2149 }
2150 else {
2151 $formValues[$key] = $greetingOptions[$key][$option];
2152 }
2153 }
2154 }
2155 }
2156 if (!empty($formValues['postal_greeting'])) {
2157 $this->setPostalGreetingTemplate($formValues['postal_greeting']);
2158 }
2159 if (!empty($formValues['addressee'])) {
2160 $this->setAddresseeGreetingTemplate($formValues['addressee']);
2161 }
2162 }
2163
3e644436 2164 /**
2165 * Create the temporary table for output.
2166 */
2167 public function createTempTable() {
2168 //creating a temporary table for the search result that need be exported
2169 $exportTempTable = CRM_Utils_SQL_TempTable::build()->setDurable()->setCategory('export');
2170 $sqlColumns = $this->getSQLColumns();
2171 // also create the sql table
2172 $exportTempTable->drop();
2173
2174 $sql = " id int unsigned NOT NULL AUTO_INCREMENT, ";
2175 if (!empty($sqlColumns)) {
2176 $sql .= implode(",\n", array_values($sqlColumns)) . ',';
2177 }
2178
2179 $sql .= "\n PRIMARY KEY ( id )";
2180
2181 // add indexes for street_address and household_name if present
2182 $addIndices = [
2183 'street_address',
2184 'household_name',
2185 'civicrm_primary_id',
2186 ];
2187
2188 foreach ($addIndices as $index) {
2189 if (isset($sqlColumns[$index])) {
2190 $sql .= ",
2191 INDEX index_{$index}( $index )
2192";
2193 }
2194 }
2195
2196 $exportTempTable->createWithColumns($sql);
2197 $this->setTemporaryTable($exportTempTable->getName());
2198 }
2199
b0b40cd7 2200 /**
2201 * Get the values of linked household contact.
2202 *
2203 * @param CRM_Core_DAO $relDAO
2204 * @param array $value
2205 * @param string $field
2206 * @param array $row
2207 *
2208 * @throws \Exception
2209 */
2210 public function fetchRelationshipDetails($relDAO, $value, $field, &$row) {
2211 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
2212 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
2213 $i18n = CRM_Core_I18n::singleton();
2214 $field = $field . '_';
2215
2216 foreach ($value as $relationField => $relationValue) {
2217 if (is_object($relDAO) && property_exists($relDAO, $relationField)) {
2218 $fieldValue = $relDAO->$relationField;
2219 if ($relationField == 'phone_type_id') {
2220 $fieldValue = $phoneTypes[$relationValue];
2221 }
2222 elseif ($relationField == 'provider_id') {
2223 $fieldValue = CRM_Utils_Array::value($relationValue, $imProviders);
2224 }
2225 // CRM-13995
2226 elseif (is_object($relDAO) && in_array($relationField, [
2227 'email_greeting',
2228 'postal_greeting',
2229 'addressee',
2230 ])) {
2231 //special case for greeting replacement
2232 $fldValue = "{$relationField}_display";
2233 $fieldValue = $relDAO->$fldValue;
2234 }
2235 }
2236 elseif (is_object($relDAO) && $relationField == 'state_province') {
2237 $fieldValue = CRM_Core_PseudoConstant::stateProvince($relDAO->state_province_id);
2238 }
2239 elseif (is_object($relDAO) && $relationField == 'country') {
2240 $fieldValue = CRM_Core_PseudoConstant::country($relDAO->country_id);
2241 }
2242 else {
2243 $fieldValue = '';
2244 }
2245 $relPrefix = $field . $relationField;
2246
2247 if (is_object($relDAO) && $relationField == 'id') {
2248 $row[$relPrefix] = $relDAO->contact_id;
2249 }
2250 elseif (is_array($relationValue) && $relationField == 'location') {
2251 foreach ($relationValue as $ltype => $val) {
2252 // If the location name has a space in it the we need to handle that. This
2253 // is kinda hacky but specifically covered in the ExportTest so later efforts to
2254 // improve it should be secure in the knowled it will be caught.
2255 $ltype = str_replace(' ', '_', $ltype);
2256 foreach (array_keys($val) as $fld) {
2257 $type = explode('-', $fld);
2258 $fldValue = "{$ltype}-" . $type[0];
2259 if (!empty($type[1])) {
2260 $fldValue .= "-" . $type[1];
2261 }
2262 // CRM-3157: localise country, region (both have ‘country’ context)
2263 // and state_province (‘province’ context)
2264 switch (TRUE) {
2265 case (!is_object($relDAO)):
2266 $row[$field . '_' . $fldValue] = '';
2267 break;
2268
2269 case in_array('country', $type):
2270 case in_array('world_region', $type):
2271 $row[$field . '_' . $fldValue] = $i18n->crm_translate($relDAO->$fldValue,
2272 ['context' => 'country']
2273 );
2274 break;
2275
2276 case in_array('state_province', $type):
2277 $row[$field . '_' . $fldValue] = $i18n->crm_translate($relDAO->$fldValue,
2278 ['context' => 'province']
2279 );
2280 break;
2281
2282 default:
2283 $row[$field . '_' . $fldValue] = $relDAO->$fldValue;
2284 break;
2285 }
2286 }
2287 }
2288 }
2289 elseif (isset($fieldValue) && $fieldValue != '') {
2290 //check for custom data
2291 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($relationField)) {
2292 $row[$relPrefix] = CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
2293 }
2294 else {
2295 //normal relationship fields
2296 // CRM-3157: localise country, region (both have ‘country’ context) and state_province (‘province’ context)
2297 switch ($relationField) {
2298 case 'country':
2299 case 'world_region':
2300 $row[$relPrefix] = $i18n->crm_translate($fieldValue, ['context' => 'country']);
2301 break;
2302
2303 case 'state_province':
2304 $row[$relPrefix] = $i18n->crm_translate($fieldValue, ['context' => 'province']);
2305 break;
2306
2307 default:
2308 $row[$relPrefix] = $fieldValue;
2309 break;
2310 }
2311 }
2312 }
2313 else {
2314 // if relation field is empty or null
2315 $row[$relPrefix] = '';
2316 }
2317 }
2318 }
2319
b6149515 2320 /**
2321 * Write to the csv from the temp table.
2322 */
2323 public function writeCSVFromTable() {
2324 // call export hook
2325 $headerRows = $this->getHeaderRows();
2326 $exportTempTable = $this->getTemporaryTable();
246e571e
JG
2327 $exportMode = $this->getExportMode();
2328 $sqlColumns = $this->getSQLColumns();
2329 $componentTable = $this->getComponentTable();
2330 $ids = $this->getIds();
b6149515 2331 CRM_Utils_Hook::export($exportTempTable, $headerRows, $sqlColumns, $exportMode, $componentTable, $ids);
246e571e
JG
2332 if ($exportMode !== $this->getExportMode() || $componentTable !== $this->getComponentTable()) {
2333 CRM_Core_Error::deprecatedFunctionWarning('altering the export mode and/or component table in the hook is no longer supported.');
2334 }
ca934663
SL
2335 if ($ids !== $this->getIds()) {
2336 CRM_Core_Error::deprecatedFunctionWarning('altering the ids in the hook is no longer supported.');
2337 }
b6149515 2338 if ($exportTempTable !== $this->getTemporaryTable()) {
2339 CRM_Core_Error::deprecatedFunctionWarning('altering the export table in the hook is deprecated (in some flows the table itself will be)');
2340 $this->setTemporaryTable($exportTempTable);
2341 }
2342 $exportTempTable = $this->getTemporaryTable();
2343 $writeHeader = TRUE;
2344 $offset = 0;
2345 // increase this number a lot to avoid making too many queries
2346 // LIMIT is not much faster than a no LIMIT query
2347 // CRM-7675
2348 $limit = 100000;
2349
2350 $query = "SELECT * FROM $exportTempTable";
2351
e2ea63f5 2352 $this->instantiateTempTable($headerRows);
b6149515 2353 while (1) {
2354 $limitQuery = $query . "
2355LIMIT $offset, $limit
2356";
2357 $dao = CRM_Core_DAO::executeQuery($limitQuery);
2358
2359 if ($dao->N <= 0) {
2360 break;
2361 }
2362
2363 $componentDetails = [];
2364 while ($dao->fetch()) {
2365 $row = [];
2366
246e571e 2367 foreach (array_keys($sqlColumns) as $column) {
b6149515 2368 $row[$column] = $dao->$column;
2369 }
2370 $componentDetails[] = $row;
2371 }
e2ea63f5 2372 $this->writeRows($headerRows, $componentDetails);
2373
b6149515 2374 $offset += $limit;
2375 }
2376 }
2377
e2ea63f5 2378 /**
2379 * Set up the temp table.
2380 *
2381 * @param array $headerRows
2382 */
2383 protected function instantiateTempTable(array $headerRows) {
2384 CRM_Utils_System::download(CRM_Utils_String::munge($this->getExportFileName()),
2385 'text/x-csv',
2386 CRM_Core_DAO::$_nullObject,
2387 'csv',
2388 FALSE
2389 );
2390 CRM_Core_Report_Excel::makeCSVTable($headerRows, [], NULL, TRUE, TRUE);
2391 }
2392
2393 /**
2394 * @param array $headerRows
2395 * @param array $componentDetails
2396 */
2397 protected function writeRows(array $headerRows, array $componentDetails) {
2398 CRM_Core_Report_Excel::writeCSVFile($this->getExportFileName(),
2399 $headerRows,
2400 $componentDetails,
2401 NULL,
2402 FALSE
2403 );
2404 }
2405
cf7c5cb8 2406 /**
2407 * Cache the greeting fields for the given contact.
2408 *
2409 * @param int $contactID
2410 */
2411 protected function cacheContactGreetings(int $contactID) {
2412 if (!isset($this->contactGreetingFields[$contactID])) {
2413 $this->contactGreetingFields[$contactID] = $this->replaceMergeTokens($contactID);
2414 }
2415 }
2416
2417 /**
2418 * Get the greeting value for the given contact.
2419 *
2420 * The values have already been cached so we are grabbing the value at this point.
2421 *
2422 * @param int $contactID
2423 * @param string $type
2424 * postal_greeting|addressee|email_greeting
2425 * @param string $default
2426 *
2427 * @return string
2428 */
2429 protected function getContactGreeting(int $contactID, string $type, string $default) {
2430 return CRM_Utils_Array::value($type,
2431 $this->contactGreetingFields[$contactID], $default
2432 );
2433 }
2434
d183e23a 2435 /**
2436 * Get the portion of the greeting string that relates to the contact.
2437 *
2438 * For example if the greeting id 'Dear Sarah' we are going to combine it with 'Dear Mike'
2439 * so we want to strip the 'Dear ' and just get 'Sarah
2440 * @param int $contactID
2441 * @param int $greetingID
2442 * @param string $type
2443 * postal_greeting, addressee (email_greeting not currently implemented for unknown reasons.
2444 * @param string $defaultGreeting
2445 *
2446 * @return mixed|string
2447 */
2448 protected function getContactPortionOfGreeting(int $contactID, int $greetingID, string $type, string $defaultGreeting) {
2449 $copyPostalGreeting = $this->getContactGreeting($contactID, $type, $defaultGreeting);
2450 $template = $type === 'postal_greeting' ? $this->getPostalGreetingTemplate() : $this->getAddresseeGreetingTemplate();
2451 if ($copyPostalGreeting) {
2452 $copyPostalGreeting = $this->trimNonTokensFromAddressString($copyPostalGreeting,
2453 $this->greetingOptions[$type][$greetingID],
2454 $template
2455 );
2456 }
2457 return $copyPostalGreeting;
2458 }
2459
6003a964 2460}