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