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