Merge pull request #14810 from eileenmcnaughton/fix_up
[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
96 /**
97 * Get additional non-visible fields for address merge purposes.
98 *
99 * @return array
100 */
101 public function getAdditionalFieldsForSameAddressMerge(): array {
102 return $this->additionalFieldsForSameAddressMerge;
103 }
104
105 /**
106 * Set additional non-visible fields for address merge purposes.
107 */
108 public function setAdditionalFieldsForSameAddressMerge() {
109 if ($this->isMergeSameAddress) {
110 $fields = ['id', 'master_id', 'state_province_id', 'postal_greeting_id', 'addressee_id'];
111 foreach ($fields as $index => $field) {
112 if (!empty($this->getReturnProperties()[$field])) {
113 unset($fields[$index]);
114 }
115 }
116 $this->additionalFieldsForSameAddressMerge = array_fill_keys($fields, 1);
117 }
118 }
119
120 /**
121 * Should contacts with the same address be merged.
122 *
123 * @return bool
124 */
125 public function isMergeSameAddress(): bool {
126 return $this->isMergeSameAddress;
127 }
128
129 /**
130 * Set same address is to be merged.
131 *
132 * @param bool $isMergeSameAddress
133 */
134 public function setIsMergeSameAddress(bool $isMergeSameAddress) {
135 $this->isMergeSameAddress = $isMergeSameAddress;
136 }
137
51d84eca 138 /**
139 * Additional fields required to export postal fields.
140 *
141 * @var array
142 */
143 protected $additionalFieldsForPostalExport = [];
144
145 /**
146 * Get additional fields required to do a postal export.
147 *
148 * @return array
149 */
150 public function getAdditionalFieldsForPostalExport() {
151 return $this->additionalFieldsForPostalExport;
152 }
153
154 /**
155 * Set additional fields required for a postal export.
156 */
157 public function setAdditionalFieldsForPostalExport() {
158 if ($this->getRequestedFields() && $this->isPostalableOnly()) {
159 $fields = ['is_deceased', 'do_not_mail', 'street_address', 'supplemental_address_1'];
160 foreach ($fields as $index => $field) {
161 if (!empty($this->getReturnProperties()[$field])) {
162 unset($field[$index]);
163 }
164 }
165 $this->additionalFieldsForPostalExport = array_fill_keys($fields, 1);
166 }
167 }
168
6d52bfe5 169 /**
170 * Only export contacts that can receive postal mail.
171 *
172 * Includes being alive, having an address & not having do_not_mail.
173 *
174 * @var bool
175 */
176 protected $isPostalableOnly;
177
944ed388 178 /**
179 * Key representing the head of household in the relationship array.
180 *
181 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
182 *
7b966967 183 * @var array
944ed388 184 */
185 protected $relationshipTypes = [];
186
704e3e9a 187 /**
188 * Array of properties to retrieve for relationships.
189 *
190 * @var array
191 */
192 protected $relationshipReturnProperties = [];
193
136f69a8 194 /**
195 * IDs of households that have already been exported.
196 *
197 * @var array
198 */
199 protected $exportedHouseholds = [];
200
ebebd629 201 /**
202 * Households to skip during export as they will be exported via their relationships anyway.
203 *
204 * @var array
205 */
206 protected $householdsToSkip = [];
207
e530cef4 208 /**
209 * Additional fields to return.
210 *
211 * This doesn't make much sense when we have a fields set but search build add it's own onto
212 * the 'Primary fields' (all) option.
213 *
214 * @var array
215 */
216 protected $additionalRequestedReturnProperties = [];
217
218 /**
219 * Get additional return properties.
220 *
221 * @return array
222 */
223 public function getAdditionalRequestedReturnProperties() {
224 return $this->additionalRequestedReturnProperties;
225 }
226
227 /**
228 * Set additional return properties.
229 *
230 * @param array $value
231 */
232 public function setAdditionalRequestedReturnProperties($value) {
233 // fix for CRM-7066
234 if (!empty($value['group'])) {
235 unset($value['group']);
236 $value['groups'] = 1;
237 }
238 $this->additionalRequestedReturnProperties = $value;
239 }
240
ce12a9e0 241 /**
242 * Get return properties by relationship.
243 * @return array
244 */
245 public function getRelationshipReturnProperties() {
246 return $this->relationshipReturnProperties;
247 }
248
249 /**
250 * Export values for related contacts.
251 *
252 * @var array
253 */
254 protected $relatedContactValues = [];
255
c66a5741 256 /**
257 * @var array
258 */
259 protected $returnProperties = [];
260
28254dcb 261 /**
262 * @var array
263 */
264 protected $outputSpecification = [];
265
e034ee91 266 /**
267 * @var string
268 */
269 protected $componentTable = '';
270
271 /**
272 * @return string
273 */
274 public function getComponentTable() {
275 return $this->componentTable;
276 }
277
278 /**
279 * Set the component table (if any).
280 *
281 * @param string $componentTable
282 */
283 public function setComponentTable($componentTable) {
284 $this->componentTable = $componentTable;
285 }
286
287 /**
288 * Clause from component search.
289 *
290 * @var string
291 */
292 protected $componentClause = '';
293
294 /**
295 * @return string
296 */
297 public function getComponentClause() {
298 return $this->componentClause;
299 }
300
301 /**
302 * @param string $componentClause
303 */
304 public function setComponentClause($componentClause) {
305 $this->componentClause = $componentClause;
306 }
307
68989e71 308 /**
309 * Name of a temporary table created to hold the results.
310 *
311 * Current decision making on when to create a temp table is kinda bad so this might change
312 * a bit as it is reviewed but basically we need a temp table or similar to calculate merging
313 * addresses. Merging households is handled in php. We create a temp table even when we don't need them.
314 *
315 * @var string
316 */
317 protected $temporaryTable;
318
319 /**
320 * @return string
321 */
322 public function getTemporaryTable(): string {
323 return $this->temporaryTable;
324 }
325
326 /**
327 * @param string $temporaryTable
328 */
329 public function setTemporaryTable(string $temporaryTable) {
330 $this->temporaryTable = $temporaryTable;
331 }
332
71464b73 333 /**
334 * CRM_Export_BAO_ExportProcessor constructor.
335 *
336 * @param int $exportMode
041ecc95 337 * @param array|null $requestedFields
71464b73 338 * @param string $queryOperator
b7db6051 339 * @param bool $isMergeSameHousehold
6d52bfe5 340 * @param bool $isPostalableOnly
2761a916 341 * @param bool $isMergeSameAddress
71464b73 342 */
2761a916 343 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE, $isMergeSameAddress = FALSE) {
71464b73 344 $this->setExportMode($exportMode);
345 $this->setQueryMode();
346 $this->setQueryOperator($queryOperator);
d41ab886 347 $this->setRequestedFields($requestedFields);
944ed388 348 $this->setRelationshipTypes();
b7db6051 349 $this->setIsMergeSameHousehold($isMergeSameHousehold);
806c5e1e 350 $this->setIsPostalableOnly($isPostalableOnly);
2761a916 351 $this->setIsMergeSameAddress($isMergeSameAddress);
806c5e1e 352 $this->setReturnProperties($this->determineReturnProperties());
2761a916 353 $this->setAdditionalFieldsForSameAddressMerge();
51d84eca 354 $this->setAdditionalFieldsForPostalExport();
3611a532 355 $this->setHouseholdMergeReturnProperties();
d41ab886 356 }
357
6d52bfe5 358 /**
359 * @return bool
360 */
361 public function isPostalableOnly() {
362 return $this->isPostalableOnly;
363 }
364
365 /**
366 * @param bool $isPostalableOnly
367 */
368 public function setIsPostalableOnly($isPostalableOnly) {
369 $this->isPostalableOnly = $isPostalableOnly;
370 }
7b966967 371
d41ab886 372 /**
373 * @return array|null
374 */
375 public function getRequestedFields() {
04f8f758 376 return empty($this->requestedFields) ? NULL : $this->requestedFields;
d41ab886 377 }
378
379 /**
380 * @param array|null $requestedFields
381 */
382 public function setRequestedFields($requestedFields) {
383 $this->requestedFields = $requestedFields;
71464b73 384 }
385
c66a5741 386 /**
387 * @return array
388 */
389 public function getReturnProperties() {
51d84eca 390 return array_merge($this->returnProperties, $this->getAdditionalRequestedReturnProperties(), $this->getAdditionalFieldsForSameAddressMerge(), $this->getAdditionalFieldsForPostalExport());
c66a5741 391 }
392
393 /**
394 * @param array $returnProperties
395 */
396 public function setReturnProperties($returnProperties) {
397 $this->returnProperties = $returnProperties;
398 }
399
944ed388 400 /**
401 * @return array
402 */
403 public function getRelationshipTypes() {
404 return $this->relationshipTypes;
405 }
406
407 /**
408 */
409 public function setRelationshipTypes() {
410 $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
411 NULL,
412 NULL,
413 NULL,
414 NULL,
415 TRUE,
416 'name',
417 FALSE
418 );
419 }
420
ce12a9e0 421 /**
422 * Set the value for a relationship type field.
423 *
424 * In this case we are building up an array of properties for a related contact.
425 *
426 * These may be used for direct exporting or for merge to household depending on the
427 * options selected.
428 *
429 * @param string $relationshipType
430 * @param int $contactID
431 * @param string $field
432 * @param string $value
433 */
434 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
435 $this->relatedContactValues[$relationshipType][$contactID][$field] = $value;
ebebd629 436 if ($field === 'id') {
437 $this->householdsToSkip[] = $value;
438 }
ce12a9e0 439 }
440
441 /**
442 * Get the value for a relationship type field.
443 *
444 * In this case we are building up an array of properties for a related contact.
445 *
446 * These may be used for direct exporting or for merge to household depending on the
447 * options selected.
448 *
449 * @param string $relationshipType
450 * @param int $contactID
451 * @param string $field
452 *
453 * @return string
454 */
455 public function getRelationshipValue($relationshipType, $contactID, $field) {
456 return isset($this->relatedContactValues[$relationshipType][$contactID][$field]) ? $this->relatedContactValues[$relationshipType][$contactID][$field] : '';
457 }
458
136f69a8 459 /**
460 * Get the id of the related household.
461 *
462 * @param int $contactID
463 * @param string $relationshipType
464 *
465 * @return int
466 */
467 public function getRelatedHouseholdID($contactID, $relationshipType) {
468 return $this->relatedContactValues[$relationshipType][$contactID]['id'];
469 }
470
471 /**
472 * Has the household already been exported.
473 *
474 * @param int $housholdContactID
475 *
476 * @return bool
477 */
478 public function isHouseholdExported($housholdContactID) {
479 return isset($this->exportedHouseholds[$housholdContactID]);
480
481 }
482
b7db6051 483 /**
484 * @return bool
485 */
486 public function isMergeSameHousehold() {
487 return $this->isMergeSameHousehold;
488 }
489
490 /**
491 * @param bool $isMergeSameHousehold
492 */
493 public function setIsMergeSameHousehold($isMergeSameHousehold) {
494 $this->isMergeSameHousehold = $isMergeSameHousehold;
495 }
496
497 /**
498 * Return relationship types for household merge.
499 *
500 * @return mixed
501 */
502 public function getHouseholdRelationshipTypes() {
503 if (!$this->isMergeSameHousehold()) {
504 return [];
505 }
506 return [
507 CRM_Utils_Array::key('Household Member of', $this->getRelationshipTypes()),
508 CRM_Utils_Array::key('Head of Household for', $this->getRelationshipTypes()),
509 ];
510 }
944ed388 511
512 /**
513 * @param $fieldName
514 * @return bool
515 */
516 public function isRelationshipTypeKey($fieldName) {
517 return array_key_exists($fieldName, $this->relationshipTypes);
518 }
519
b7db6051 520 /**
521 * @param $fieldName
522 * @return bool
523 */
524 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
525 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
526 }
527
71464b73 528 /**
529 * @return string
530 */
531 public function getQueryOperator() {
532 return $this->queryOperator;
533 }
534
535 /**
536 * @param string $queryOperator
537 */
538 public function setQueryOperator($queryOperator) {
539 $this->queryOperator = $queryOperator;
540 }
541
adabfa40 542 /**
543 * @return array
544 */
545 public function getQueryFields() {
546 return $this->queryFields;
547 }
548
549 /**
550 * @param array $queryFields
551 */
552 public function setQueryFields($queryFields) {
5ebd7d09 553 // legacy hacks - we add these to queryFields because this
554 // pseudometadata is currently required.
555 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
556 $queryFields['country']['context'] = 'country';
557 $queryFields['world_region']['context'] = 'country';
558 $queryFields['state_province']['context'] = 'province';
adabfa40 559 $this->queryFields = $queryFields;
560 }
561
6003a964 562 /**
563 * @return int
564 */
565 public function getQueryMode() {
566 return $this->queryMode;
567 }
568
569 /**
570 * Set the query mode based on the export mode.
571 */
572 public function setQueryMode() {
573
574 switch ($this->getExportMode()) {
575 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
576 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE;
577 break;
578
579 case CRM_Export_Form_Select::EVENT_EXPORT:
580 $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT;
581 break;
582
583 case CRM_Export_Form_Select::MEMBER_EXPORT:
584 $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER;
585 break;
586
587 case CRM_Export_Form_Select::PLEDGE_EXPORT:
588 $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE;
589 break;
590
591 case CRM_Export_Form_Select::CASE_EXPORT:
592 $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE;
593 break;
594
595 case CRM_Export_Form_Select::GRANT_EXPORT:
596 $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT;
597 break;
598
599 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
600 $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY;
601 break;
602
603 default:
604 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS;
605 }
606 }
607
608 /**
609 * @return int
610 */
611 public function getExportMode() {
612 return $this->exportMode;
613 }
614
615 /**
616 * @param int $exportMode
617 */
618 public function setExportMode($exportMode) {
619 $this->exportMode = $exportMode;
620 }
621
29034a98 622 /**
623 * Get the name for the export file.
624 *
625 * @return string
626 */
627 public function getExportFileName() {
628 switch ($this->getExportMode()) {
629 case CRM_Export_Form_Select::CONTACT_EXPORT:
630 return ts('CiviCRM Contact Search');
631
632 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
633 return ts('CiviCRM Contribution Search');
634
635 case CRM_Export_Form_Select::MEMBER_EXPORT:
636 return ts('CiviCRM Member Search');
637
638 case CRM_Export_Form_Select::EVENT_EXPORT:
639 return ts('CiviCRM Participant Search');
640
641 case CRM_Export_Form_Select::PLEDGE_EXPORT:
642 return ts('CiviCRM Pledge Search');
643
644 case CRM_Export_Form_Select::CASE_EXPORT:
645 return ts('CiviCRM Case Search');
646
647 case CRM_Export_Form_Select::GRANT_EXPORT:
648 return ts('CiviCRM Grant Search');
649
650 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
651 return ts('CiviCRM Activity Search');
652
653 default:
654 // Legacy code suggests the value could be 'financial' - ie. something
655 // other than what should be accepted. However, I suspect that this line is
656 // never hit.
657 return ts('CiviCRM Search');
658 }
659 }
660
af17bedf 661 /**
662 * Get the label for the header row based on the field to output.
663 *
664 * @param string $field
665 *
666 * @return string
667 */
668 public function getHeaderForRow($field) {
669 if (substr($field, -11) == 'campaign_id') {
670 // @todo - set this correctly in the xml rather than here.
671 // This will require a generalised handling cleanup
672 return ts('Campaign ID');
673 }
674 if ($this->isMergeSameHousehold() && $field === 'id') {
675 return ts('Household ID');
676 }
677 elseif (isset($this->getQueryFields()[$field]['title'])) {
678 return $this->getQueryFields()[$field]['title'];
679 }
680 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
681 return CRM_Utils_Array::value($field, $this->getcomponentPaymentFields());
682 }
683 else {
684 return $field;
685 }
686 }
687
adabfa40 688 /**
689 * @param $params
690 * @param $order
6df77635 691 *
adabfa40 692 * @return array
693 */
6df77635 694 public function runQuery($params, $order) {
695 $returnProperties = $this->getReturnProperties();
6d52bfe5 696 $addressWhere = '';
697 $params = array_merge($params, $this->getWhereParams());
698 if ($this->isPostalableOnly) {
699 if (array_key_exists('street_address', $returnProperties)) {
700 $addressWhere = " civicrm_address.street_address <> ''";
701 if (array_key_exists('supplemental_address_1', $returnProperties)) {
702 // We need this to be an OR rather than AND on the street_address so, hack it in.
703 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
704 'address_options', TRUE, NULL, TRUE
705 );
706 if (!empty($addressOptions['supplemental_address_1'])) {
707 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
708 }
709 }
710 $addressWhere = ' AND (' . $addressWhere . ')';
711 }
712 }
adabfa40 713 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
714 FALSE, FALSE, $this->getQueryMode(),
71464b73 715 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
adabfa40 716 );
717
718 //sort by state
719 //CRM-15301
720 $query->_sort = $order;
721 list($select, $from, $where, $having) = $query->query();
722 $this->setQueryFields($query->_fields);
e034ee91 723 $whereClauses = ['trash_clause' => "contact_a.is_deleted != 1"];
9858dc0b 724 if ($this->getRequestedFields() && ($this->getComponentTable())) {
e034ee91 725 $from .= " INNER JOIN " . $this->getComponentTable() . " ctTable ON ctTable.contact_id = contact_a.id ";
726 }
727 elseif ($this->getComponentClause()) {
728 $whereClauses[] = $this->getComponentClause();
729 }
730
731 // CRM-13982 - check if is deleted
732 foreach ($params as $value) {
733 if ($value[0] == 'contact_is_deleted') {
734 unset($whereClauses['trash_clause']);
735 }
736 }
737
738 if (empty($where)) {
739 $where = "WHERE " . implode(' AND ', $whereClauses);
740 }
741 else {
742 $where .= " AND " . implode(' AND ', $whereClauses);
743 }
9858dc0b 744 return [$query, "$select $from $where $having"];
adabfa40 745 }
746
28254dcb 747 /**
748 * Add a row to the specification for how to output data.
ae5d4caf 749 *
28254dcb 750 * @param string $key
28254dcb 751 * @param string $relationshipType
ae5d4caf 752 * @param string $locationType
753 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
28254dcb 754 */
ae5d4caf 755 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
5ebd7d09 756 $entityLabel = '';
ae5d4caf 757 if ($entityTypeID) {
758 if ($key === 'phone') {
5ebd7d09 759 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
ae5d4caf 760 }
761 if ($key === 'im') {
5ebd7d09 762 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
ae5d4caf 763 }
764 }
ae5d4caf 765
5ebd7d09 766 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
767 // but in time it may be good to rationalise them.
768 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
769 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
770 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
771
772 $this->outputSpecification[$index]['header'] = $label;
773 $this->outputSpecification[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
774
775 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
776 $this->setColumnAsCalculationOnly($index);
777 }
778 $this->outputSpecification[$index]['metadata'] = $this->getMetaDataForField($key);
779 }
780
781 /**
782 * Get the metadata for the given field.
783 *
784 * @param $key
785 *
786 * @return array
787 */
788 public function getMetaDataForField($key) {
789 $mappings = ['contact_id' => 'id'];
790 if (isset($this->getQueryFields()[$key])) {
791 return $this->getQueryFields()[$key];
792 }
793 if (isset($mappings[$key])) {
794 return $this->getQueryFields()[$mappings[$key]];
795 }
796 return [];
2cd3d767 797 }
ae5d4caf 798
2cd3d767 799 /**
800 * @param $key
801 */
802 public function setSqlColumnDefn($key) {
5ebd7d09 803 $this->outputSpecification[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
de6ff509 804 }
805
806 /**
807 * Mark a column as only required for calculations.
808 *
809 * Do not include the row with headers.
810 *
811 * @param string $column
812 */
813 public function setColumnAsCalculationOnly($column) {
814 $this->outputSpecification[$column]['do_not_output_to_csv'] = TRUE;
28254dcb 815 }
816
817 /**
818 * @return array
819 */
820 public function getHeaderRows() {
821 $headerRows = [];
822 foreach ($this->outputSpecification as $key => $spec) {
de6ff509 823 if (empty($spec['do_not_output_to_csv'])) {
824 $headerRows[] = $spec['header'];
825 }
28254dcb 826 }
827 return $headerRows;
828 }
829
2cd3d767 830 /**
831 * @return array
832 */
833 public function getSQLColumns() {
834 $sqlColumns = [];
835 foreach ($this->outputSpecification as $key => $spec) {
836 if (empty($spec['do_not_output_to_sql'])) {
837 $sqlColumns[$key] = $spec['sql_columns'];
838 }
839 }
840 return $sqlColumns;
841 }
842
5ebd7d09 843 /**
844 * @return array
845 */
846 public function getMetadata() {
847 $metadata = [];
848 foreach ($this->outputSpecification as $key => $spec) {
849 $metadata[$key] = $spec['metadata'];
850 }
851 return $metadata;
852 }
2cd3d767 853
6e2de55d 854 /**
855 * Build the row for output.
856 *
857 * @param \CRM_Contact_BAO_Query $query
858 * @param CRM_Core_DAO $iterationDAO
859 * @param array $outputColumns
860 * @param $metadata
861 * @param $paymentDetails
862 * @param $addPaymentHeader
f830f606 863 * @param \CRM_Export_BAO_ExportProcessor $processor
6e2de55d 864 *
136f69a8 865 * @return array|bool
6e2de55d 866 */
f830f606 867 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader, $processor) {
868 $paymentTableId = $processor->getPaymentTableID();
ebebd629 869 if ($this->isHouseholdToSkip($iterationDAO->contact_id)) {
870 return FALSE;
871 }
6e2de55d 872 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
873 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
874
875 $row = [];
136f69a8 876 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id);
877 if ($householdMergeRelationshipType) {
878 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id, $householdMergeRelationshipType);
879 if ($this->isHouseholdExported($householdID)) {
880 return FALSE;
881 }
882 foreach (array_keys($outputColumns) as $column) {
883 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id, $column);
884 }
885 $this->markHouseholdExported($householdID);
886 return $row;
887 }
888
6e2de55d 889 $query->convertToPseudoNames($iterationDAO);
890
891 //first loop through output columns so that we return what is required, and in same order.
892 foreach ($outputColumns as $field => $value) {
6e2de55d 893 // add im_provider to $dao object
894 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
895 $iterationDAO->im_provider = $iterationDAO->provider_id;
896 }
897
898 //build row values (data)
899 $fieldValue = NULL;
900 if (property_exists($iterationDAO, $field)) {
901 $fieldValue = $iterationDAO->$field;
902 // to get phone type from phone type id
903 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
904 $fieldValue = $phoneTypes[$fieldValue];
905 }
906 elseif ($field == 'provider_id' || $field == 'im_provider') {
907 $fieldValue = CRM_Utils_Array::value($fieldValue, $imProviders);
908 }
909 elseif (strstr($field, 'master_id')) {
910 $masterAddressId = NULL;
911 if (isset($iterationDAO->$field)) {
912 $masterAddressId = $iterationDAO->$field;
913 }
914 // get display name of contact that address is shared.
915 $fieldValue = CRM_Contact_BAO_Contact::getMasterDisplayName($masterAddressId);
916 }
917 }
918
919 if ($this->isRelationshipTypeKey($field)) {
136f69a8 920 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id, $value, $field);
6e2de55d 921 }
922 else {
923 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
924 }
925 }
926
927 // If specific payment fields have been selected for export, payment
928 // data will already be in $row. Otherwise, add payment related
929 // information, if appropriate.
930 if ($addPaymentHeader) {
931 if (!$this->isExportSpecifiedPaymentFields()) {
932 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
933 if ($this->isExportPaymentFields()) {
934 $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails);
935 if (!is_array($paymentData) || empty($paymentData)) {
936 $paymentData = $nullContributionDetails;
937 }
938 $row = array_merge($row, $paymentData);
939 }
940 elseif (!empty($paymentDetails)) {
941 $row = array_merge($row, $nullContributionDetails);
942 }
943 }
944 }
945 //remove organization name for individuals if it is set for current employer
946 if (!empty($row['contact_type']) &&
947 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
948 ) {
949 $row['organization_name'] = '';
950 }
951 return $row;
952 }
953
136f69a8 954 /**
955 * If this row has a household whose details we should use get the relationship type key.
956 *
957 * @param $contactID
958 *
959 * @return bool
960 */
961 public function getHouseholdMergeTypeForRow($contactID) {
962 if (!$this->isMergeSameHousehold()) {
963 return FALSE;
964 }
965 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
966 if (isset($this->relatedContactValues[$relationshipType][$contactID])) {
967 return $relationshipType;
968 }
969 }
970 }
971
972 /**
973 * Mark the given household as already exported.
974 *
975 * @param $householdID
976 */
977 public function markHouseholdExported($householdID) {
978 $this->exportedHouseholds[$householdID] = $householdID;
979 }
980
8bd67f47 981 /**
982 * @param $field
983 * @param $iterationDAO
984 * @param $fieldValue
985 * @param $metadata
986 * @param $paymentDetails
987 *
988 * @return string
989 */
990 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
991
992 $i18n = CRM_Core_I18n::singleton();
993 if ($field == 'id') {
994 return $iterationDAO->contact_id;
995 // special case for calculated field
996 }
997 elseif ($field == 'source_contact_id') {
998 return $iterationDAO->contact_id;
999 }
1000 elseif ($field == 'pledge_balance_amount') {
1001 return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid;
1002 // special case for calculated field
1003 }
1004 elseif ($field == 'pledge_next_pay_amount') {
1005 return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount;
1006 }
1007 elseif (isset($fieldValue) &&
1008 $fieldValue != ''
1009 ) {
1010 //check for custom data
1011 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) {
1012 return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
1013 }
1014
be2fb01f 1015 elseif (in_array($field, [
8bd67f47 1016 'email_greeting',
1017 'postal_greeting',
1018 'addressee',
be2fb01f 1019 ])) {
8bd67f47 1020 //special case for greeting replacement
1021 $fldValue = "{$field}_display";
1022 return $iterationDAO->$fldValue;
1023 }
1024 else {
1025 //normal fields with a touch of CRM-3157
1026 switch ($field) {
1027 case 'country':
1028 case 'world_region':
be2fb01f 1029 return $i18n->crm_translate($fieldValue, ['context' => 'country']);
8bd67f47 1030
1031 case 'state_province':
be2fb01f 1032 return $i18n->crm_translate($fieldValue, ['context' => 'province']);
8bd67f47 1033
1034 case 'gender':
1035 case 'preferred_communication_method':
1036 case 'preferred_mail_format':
1037 case 'communication_style':
1038 return $i18n->crm_translate($fieldValue);
1039
1040 default:
1041 if (isset($metadata[$field])) {
1042 // No I don't know why we do it this way & whether we could
1043 // make better use of pseudoConstants.
1044 if (!empty($metadata[$field]['context'])) {
1045 return $i18n->crm_translate($fieldValue, $metadata[$field]);
1046 }
1047 if (!empty($metadata[$field]['pseudoconstant'])) {
001a515b 1048 if (!empty($metadata[$field]['bao'])) {
1049 return CRM_Core_PseudoConstant::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
1050 }
8bd67f47 1051 // This is not our normal syntax for pseudoconstants but I am a bit loath to
1052 // call an external function until sure it is not increasing php processing given this
1053 // may be iterated 100,000 times & we already have the $imProvider var loaded.
1054 // That can be next refactor...
1055 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
1056 // but I have to reach a stable point....
1057 $varName = $metadata[$field]['pseudoconstant']['var'];
1058 if ($varName === 'imProviders') {
1059 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
1060 }
1061 if ($varName === 'phoneTypes') {
1062 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
1063 }
1064 }
1065
1066 }
1067 return $fieldValue;
1068 }
1069 }
1070 }
1071 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
1072 $paymentTableId = $this->getPaymentTableID();
1073 $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails);
be2fb01f 1074 $payFieldMapper = [
8bd67f47 1075 'componentPaymentField_total_amount' => 'total_amount',
1076 'componentPaymentField_contribution_status' => 'contribution_status',
1077 'componentPaymentField_payment_instrument' => 'pay_instru',
1078 'componentPaymentField_transaction_id' => 'trxn_id',
1079 'componentPaymentField_received_date' => 'receive_date',
be2fb01f 1080 ];
8bd67f47 1081 return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, '');
1082 }
1083 else {
1084 // if field is empty or null
1085 return '';
1086 }
1087 }
1088
ce14544c 1089 /**
1090 * Get array of fields to return, over & above those defined in the main contact exportable fields.
1091 *
1092 * These include export mode specific fields & some fields apparently required as 'exportableFields'
1093 * but not returned by the function of the same name.
1094 *
1095 * @return array
1096 * Array of fields to return in the format ['field_name' => 1,...]
1097 */
1098 public function getAdditionalReturnProperties() {
ce14544c 1099 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) {
1100 $componentSpecificFields = [];
1101 }
1102 else {
1103 $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode());
1104 }
1105 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) {
1106 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode()));
d28b6cf2 1107 unset($componentSpecificFields['contribution_status_id']);
1108 unset($componentSpecificFields['pledge_status_id']);
1109 unset($componentSpecificFields['pledge_payment_status_id']);
ce14544c 1110 }
1111 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) {
1112 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode()));
1113 }
1114 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) {
1115 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE));
d28b6cf2 1116 unset($componentSpecificFields['contribution_status_id']);
ce14544c 1117 }
7626754f 1118 return $componentSpecificFields;
ce14544c 1119 }
1120
d41ab886 1121 /**
1122 * Should payment fields be appended to the export.
1123 *
1124 * (This is pretty hacky so hopefully this function won't last long - notice
1125 * how obviously it should be part of the above function!).
1126 */
1127 public function isExportPaymentFields() {
1128 if ($this->getRequestedFields() === NULL
f065c170 1129 && in_array($this->getQueryMode(), [
d41ab886 1130 CRM_Contact_BAO_Query::MODE_EVENT,
1131 CRM_Contact_BAO_Query::MODE_MEMBER,
1132 CRM_Contact_BAO_Query::MODE_PLEDGE,
1133 ])) {
1134 return TRUE;
1135 }
c66a5741 1136 elseif ($this->isExportSpecifiedPaymentFields()) {
1137 return TRUE;
1138 }
d41ab886 1139 return FALSE;
1140 }
1141
c66a5741 1142 /**
1143 * Has specific payment fields been requested (as opposed to via all fields).
1144 *
1145 * If specific fields have been requested then they get added at various points.
1146 *
1147 * @return bool
1148 */
1149 public function isExportSpecifiedPaymentFields() {
1150 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
1151 return TRUE;
1152 }
1153 }
1154
d41ab886 1155 /**
1156 * Get the name of the id field in the table that connects contributions to the export entity.
1157 */
1158 public function getPaymentTableID() {
1159 if ($this->getRequestedFields() === NULL) {
1160 $mapping = [
1161 CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id',
1162 CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id',
1163 CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id',
1164 ];
1165 return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : '';
1166 }
c66a5741 1167 elseif ($this->hasRequestedComponentPaymentFields()) {
1168 return 'participant_id';
1169 }
d41ab886 1170 return FALSE;
1171 }
1172
c66a5741 1173 /**
1174 * Have component payment fields been requested.
1175 *
1176 * @return bool
1177 */
1178 protected function hasRequestedComponentPaymentFields() {
1179 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) {
1180 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
1181 if (!empty($participantPaymentFields)) {
1182 return TRUE;
1183 }
1184 }
d41ab886 1185 return FALSE;
1186 }
1187
c66a5741 1188 /**
1189 * Get fields that indicate payment fields have been requested for a component.
1190 *
0e32ed68 1191 * Ideally this should be protected but making it temporarily public helps refactoring..
1192 *
c66a5741 1193 * @return array
1194 */
0e32ed68 1195 public function getComponentPaymentFields() {
c66a5741 1196 return [
1197 'componentPaymentField_total_amount' => ts('Total Amount'),
1198 'componentPaymentField_contribution_status' => ts('Contribution Status'),
1199 'componentPaymentField_received_date' => ts('Date Received'),
1200 'componentPaymentField_payment_instrument' => ts('Payment Method'),
1201 'componentPaymentField_transaction_id' => ts('Transaction ID'),
1202 ];
1203 }
1204
05ad310f 1205 /**
1206 * Get headers for payment fields.
1207 *
1208 * Returns an array of contribution fields when the entity supports payment fields and specific fields
1209 * are not specified. This is a transitional function for refactoring legacy code.
1210 */
1211 public function getPaymentHeaders() {
1212 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
1213 return $this->getcomponentPaymentFields();
1214 }
1215 return [];
1216 }
1217
d41ab886 1218 /**
1219 * Get the default properties when not specified.
1220 *
1221 * In the UI this appears as 'Primary fields only' but in practice it's
1222 * most of the kitchen sink and the hallway closet thrown in.
1223 *
1224 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
1225 *
1226 * @return array
1227 */
1228 public function getDefaultReturnProperties() {
1229 $returnProperties = [];
1230 $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE);
704e3e9a 1231 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [
1232 'groups',
1233 'tags',
7b966967 1234 'notes',
704e3e9a 1235 ];
d41ab886 1236
1237 foreach ($fields as $key => $var) {
1238 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1239 $returnProperties[$key] = 1;
1240 }
1241 }
1242 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1243 return $returnProperties;
1244 }
1245
704e3e9a 1246 /**
1247 * Add the field to relationship return properties & return it.
1248 *
1249 * This function is doing both setting & getting which is yuck but it is an interim
1250 * refactor.
1251 *
1252 * @param array $value
1253 * @param string $relationshipKey
1254 *
1255 * @return array
1256 */
1257 public function setRelationshipReturnProperties($value, $relationshipKey) {
04f8f758 1258 $relationField = $value['name'];
1259 $relIMProviderId = NULL;
1260 $relLocTypeId = CRM_Utils_Array::value('location_type_id', $value);
1261 $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
1262 $relPhoneTypeId = CRM_Utils_Array::value('phone_type_id', $value, ($locationName ? 'Primary' : NULL));
1263 $relIMProviderId = CRM_Utils_Array::value('im_provider_id', $value, ($locationName ? 'Primary' : NULL));
1264 if (in_array($relationField, $this->getValidLocationFields()) && $locationName) {
1265 if ($relationField === 'phone') {
a16a432a 1266 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
704e3e9a 1267 }
04f8f758 1268 elseif ($relationField === 'im') {
a16a432a 1269 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
704e3e9a 1270 }
1271 else {
a16a432a 1272 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1;
704e3e9a 1273 }
1274 }
1275 else {
1276 $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1;
1277 }
1278 return $this->relationshipReturnProperties[$relationshipKey];
1279 }
1280
ce12a9e0 1281 /**
1282 * Add the main return properties to the household merge properties if needed for merging.
1283 *
1284 * If we are using household merge we need to add these to the relationship properties to
1285 * be retrieved.
ce12a9e0 1286 */
6df77635 1287 public function setHouseholdMergeReturnProperties() {
3611a532 1288 if ($this->isMergeSameHousehold()) {
1289 $returnProperties = $this->getReturnProperties();
1290 $returnProperties = array_diff_key($returnProperties, array_fill_keys(['location_type', 'im_provider'], 1));
1291 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1292 $this->relationshipReturnProperties[$householdRelationshipType] = $returnProperties;
1293 }
ce12a9e0 1294 }
1295 }
1296
704e3e9a 1297 /**
1298 * Get the default location fields to request.
1299 *
1300 * @return array
1301 */
1302 public function getValidLocationFields() {
1303 return [
1304 'street_address',
1305 'supplemental_address_1',
1306 'supplemental_address_2',
1307 'supplemental_address_3',
1308 'city',
1309 'postal_code',
1310 'postal_code_suffix',
1311 'geo_code_1',
1312 'geo_code_2',
1313 'state_province',
1314 'country',
1315 'phone',
1316 'email',
1317 'im',
1318 ];
1319 }
1320
aa3a113b 1321 /**
c8adad81 1322 * Get the sql column definition for the given field.
1323 *
5ebd7d09 1324 * @param string $fieldName
1325 * @param string $columnName
aa3a113b 1326 *
1327 * @return mixed
1328 */
5ebd7d09 1329 public function getSqlColumnDefinition($fieldName, $columnName) {
aa3a113b 1330
1331 // early exit for master_id, CRM-12100
1332 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1333 // also for current_employer, CRM-16939
5ebd7d09 1334 if ($columnName == 'master_id' || $columnName == 'current_employer') {
aa3a113b 1335 return "$fieldName varchar(128)";
1336 }
1337
1338 if (substr($fieldName, -11) == 'campaign_id') {
1339 // CRM-14398
1340 return "$fieldName varchar(128)";
1341 }
1342
1343 $queryFields = $this->getQueryFields();
1344 $lookUp = ['prefix_id', 'suffix_id'];
1345 // set the sql columns
5ebd7d09 1346 if (isset($queryFields[$columnName]['type'])) {
1347 switch ($queryFields[$columnName]['type']) {
aa3a113b 1348 case CRM_Utils_Type::T_INT:
1349 case CRM_Utils_Type::T_BOOLEAN:
5ebd7d09 1350 if (in_array($columnName, $lookUp)) {
aa3a113b 1351 return "$fieldName varchar(255)";
1352 }
1353 else {
1354 return "$fieldName varchar(16)";
1355 }
1356
1357 case CRM_Utils_Type::T_STRING:
5ebd7d09 1358 if (isset($queryFields[$columnName]['maxlength'])) {
1359 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
aa3a113b 1360 }
1361 else {
1362 return "$fieldName varchar(255)";
1363 }
1364
1365 case CRM_Utils_Type::T_TEXT:
1366 case CRM_Utils_Type::T_LONGTEXT:
1367 case CRM_Utils_Type::T_BLOB:
1368 case CRM_Utils_Type::T_MEDIUMBLOB:
1369 return "$fieldName longtext";
1370
1371 case CRM_Utils_Type::T_FLOAT:
1372 case CRM_Utils_Type::T_ENUM:
1373 case CRM_Utils_Type::T_DATE:
1374 case CRM_Utils_Type::T_TIME:
1375 case CRM_Utils_Type::T_TIMESTAMP:
1376 case CRM_Utils_Type::T_MONEY:
1377 case CRM_Utils_Type::T_EMAIL:
1378 case CRM_Utils_Type::T_URL:
1379 case CRM_Utils_Type::T_CCNUM:
1380 default:
1381 return "$fieldName varchar(32)";
1382 }
1383 }
1384 else {
1385 if (substr($fieldName, -3, 3) == '_id') {
1386 return "$fieldName varchar(255)";
1387 }
1388 elseif (substr($fieldName, -5, 5) == '_note') {
1389 return "$fieldName text";
1390 }
1391 else {
1392 $changeFields = [
1393 'groups',
1394 'tags',
1395 'notes',
1396 ];
1397
1398 if (in_array($fieldName, $changeFields)) {
1399 return "$fieldName text";
1400 }
1401 else {
1402 // set the sql columns for custom data
5ebd7d09 1403 if (isset($queryFields[$columnName]['data_type'])) {
aa3a113b 1404
5ebd7d09 1405 switch ($queryFields[$columnName]['data_type']) {
aa3a113b 1406 case 'String':
1407 // May be option labels, which could be up to 512 characters
5ebd7d09 1408 $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$columnName]));
aa3a113b 1409 return "$fieldName varchar($length)";
1410
1411 case 'Country':
1412 case 'StateProvince':
1413 case 'Link':
1414 return "$fieldName varchar(255)";
1415
1416 case 'Memo':
1417 return "$fieldName text";
1418
1419 default:
1420 return "$fieldName varchar(255)";
1421 }
1422 }
1423 else {
1424 return "$fieldName text";
1425 }
1426 }
1427 }
1428 }
1429 }
1430
c8adad81 1431 /**
1432 * Get the munged field name.
1433 *
1434 * @param string $field
1435 * @return string
1436 */
1437 public function getMungedFieldName($field) {
1438 $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64);
1439 if ($fieldName == 'id') {
1440 $fieldName = 'civicrm_primary_id';
1441 }
1442 return $fieldName;
1443 }
1444
5ebd7d09 1445 /**
1446 * In order to respect the history of this class we need to index kinda illogically.
1447 *
1448 * On the bright side - this stuff is tested within a nano-byte of it's life.
1449 *
1450 * e.g '2-a-b_Home-City'
1451 *
1452 * @param string $key
1453 * @param string $relationshipType
1454 * @param string $locationType
1455 * @param $entityLabel
1456 *
1457 * @return string
1458 */
1459 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1460 if ($entityLabel || $key === 'im') {
5ebd7d09 1461 // Just cos that's the history...
1462 if ($key !== 'master_id') {
1463 $key = $this->getHeaderForRow($key);
1464 }
1465 }
1466 if (!$relationshipType || $key !== 'id') {
1467 $key = $this->getMungedFieldName($key);
1468 }
1469 return $this->getMungedFieldName(
1470 ($relationshipType ? ($relationshipType . '_') : '')
1471 . ($locationType ? ($locationType . '_') : '')
1472 . $key
1473 . ($entityLabel ? ('_' . $entityLabel) : '')
1474 );
1475 }
1476
1477 /**
1478 * Get the compiled label for the column.
1479 *
1480 * e.g 'Gender', 'Employee Of-Home-city'
1481 *
1482 * @param string $key
1483 * @param string $relationshipType
1484 * @param string $locationType
1485 * @param string $entityLabel
1486 *
1487 * @return string
1488 */
1489 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1490 return ($relationshipType ? $this->getRelationshipTypes()[$relationshipType] . '-' : '')
1491 . ($locationType ? $locationType . '-' : '')
1492 . $this->getHeaderForRow($key)
1493 . ($entityLabel ? '-' . $entityLabel : '');
1494 }
1495
1496 /**
1497 * Get the mysql field name key.
1498 *
1499 * This key is locked in by tests but the reasons for the specific conventions -
1500 * ie. headings are used for keying fields in some cases, are likely
1501 * accidental rather than deliberate.
1502 *
1503 * This key is used for the output sql array.
1504 *
1505 * @param string $key
1506 * @param $relationshipType
1507 * @param $locationType
1508 * @param $entityLabel
1509 *
1510 * @return string
1511 */
1512 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1513 if ($entityLabel || $key === 'im') {
5ebd7d09 1514 if ($key !== 'state_province' && $key !== 'id') {
001a515b 1515 // @todo - test removing this - indexing by $key should be fine...
5ebd7d09 1516 $key = $this->getHeaderForRow($key);
1517 }
1518 }
1519 if (!$relationshipType || $key !== 'id') {
1520 $key = $this->getMungedFieldName($key);
1521 }
1522 $fieldKey = $this->getMungedFieldName(
1523 ($relationshipType ? ($relationshipType . '_') : '')
1524 . ($locationType ? ($locationType . '_') : '')
1525 . $key
1526 . ($entityLabel ? ('_' . $entityLabel) : '')
1527 );
1528 return $fieldKey;
1529 }
1530
6d52bfe5 1531 /**
1532 * Get params for the where criteria.
1533 *
1534 * @return mixed
1535 */
1536 public function getWhereParams() {
1537 if (!$this->isPostalableOnly()) {
1538 return [];
1539 }
1540 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1541 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1542 return $params;
1543 }
1544
136f69a8 1545 /**
1546 * @param $row
1547 * @param $contactID
1548 * @param $value
1549 * @param $field
1550 */
1551 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1552 foreach (array_keys($value) as $property) {
1553 if ($property === 'location') {
1554 // @todo just undo all this nasty location wrangling!
1555 foreach ($value['location'] as $locationKey => $locationFields) {
1556 foreach (array_keys($locationFields) as $locationField) {
1557 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1558 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1559 }
1560 }
1561 }
1562 else {
1563 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1564 }
1565 }
1566 }
1567
ebebd629 1568 /**
1569 * Is this contact a household that is already set to be exported by virtue of it's household members.
1570 *
1571 * @param int $contactID
1572 *
1573 * @return bool
1574 */
1575 protected function isHouseholdToSkip($contactID) {
1576 return in_array($contactID, $this->householdsToSkip);
1577 }
1578
ccc3ac8a 1579 /**
1580 * Get default return property for export based on mode
1581 *
1582 * @return string
1583 * Default Return property
1584 */
1585 public function defaultReturnProperty() {
1586 // hack to add default return property based on export mode
1587 $property = NULL;
1588 $exportMode = $this->getExportMode();
1589 if ($exportMode == CRM_Export_Form_Select::CONTRIBUTE_EXPORT) {
1590 $property = 'contribution_id';
1591 }
1592 elseif ($exportMode == CRM_Export_Form_Select::EVENT_EXPORT) {
1593 $property = 'participant_id';
1594 }
1595 elseif ($exportMode == CRM_Export_Form_Select::MEMBER_EXPORT) {
1596 $property = 'membership_id';
1597 }
1598 elseif ($exportMode == CRM_Export_Form_Select::PLEDGE_EXPORT) {
1599 $property = 'pledge_id';
1600 }
1601 elseif ($exportMode == CRM_Export_Form_Select::CASE_EXPORT) {
1602 $property = 'case_id';
1603 }
1604 elseif ($exportMode == CRM_Export_Form_Select::GRANT_EXPORT) {
1605 $property = 'grant_id';
1606 }
1607 elseif ($exportMode == CRM_Export_Form_Select::ACTIVITY_EXPORT) {
1608 $property = 'activity_id';
1609 }
1610 return $property;
1611 }
1612
806c5e1e 1613 /**
1614 * Determine the required return properties from the input parameters.
1615 *
1616 * @return array
1617 */
1618 public function determineReturnProperties() {
1619 if ($this->getRequestedFields()) {
1620 $returnProperties = [];
1621 foreach ($this->getRequestedFields() as $key => $value) {
04f8f758 1622 $fieldName = $value['name'];
7d0e773c 1623 $locationName = !empty($value['location_type_id']) ? CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $value['location_type_id']) : NULL;
04f8f758 1624 $relationshipTypeKey = !empty($value['relationship_type_id']) ? $value['relationship_type_id'] . '_' . $value['relationship_direction'] : NULL;
1625 if (!$fieldName || $this->isHouseholdMergeRelationshipTypeKey($relationshipTypeKey)) {
806c5e1e 1626 continue;
1627 }
1628
04f8f758 1629 if ($this->isRelationshipTypeKey($relationshipTypeKey)) {
1630 $returnProperties[$relationshipTypeKey] = $this->setRelationshipReturnProperties($value, $relationshipTypeKey);
806c5e1e 1631 }
04f8f758 1632 elseif ($locationName) {
1633 if ($fieldName === 'phone') {
1634 $returnProperties['location'][$locationName]['phone-' . $value['phone_type_id'] ?? NULL] = 1;
806c5e1e 1635 }
04f8f758 1636 elseif ($fieldName === 'im') {
1637 $returnProperties['location'][$locationName]['im-' . $value['im_provider_id'] ?? NULL] = 1;
806c5e1e 1638 }
1639 else {
1640 $returnProperties['location'][$locationName][$fieldName] = 1;
1641 }
1642 }
1643 else {
1644 //hack to fix component fields
1645 //revert mix of event_id and title
1646 if ($fieldName == 'event_id') {
1647 $returnProperties['event_id'] = 1;
1648 }
1649 else {
1650 $returnProperties[$fieldName] = 1;
1651 }
1652 }
1653 }
1654 $defaultExportMode = $this->defaultReturnProperty();
1655 if ($defaultExportMode) {
1656 $returnProperties[$defaultExportMode] = 1;
1657 }
1658 }
1659 else {
1660 $returnProperties = $this->getDefaultReturnProperties();
1661 }
25e51a1c 1662 if ($this->isMergeSameHousehold()) {
1663 $returnProperties['id'] = 1;
1664 }
2761a916 1665 if ($this->isMergeSameAddress()) {
1666 $returnProperties['addressee'] = 1;
1667 $returnProperties['postal_greeting'] = 1;
1668 $returnProperties['email_greeting'] = 1;
1669 $returnProperties['street_name'] = 1;
1670 $returnProperties['household_name'] = 1;
1671 $returnProperties['street_address'] = 1;
1672 $returnProperties['city'] = 1;
1673 $returnProperties['state_province'] = 1;
1674
1675 }
806c5e1e 1676 return $returnProperties;
1677 }
1678
571f3b5b 1679 /**
1680 * @param int $contactId
1681 * @param array $exportParams
1682 *
1683 * @return array
1684 */
1685 public function replaceMergeTokens($contactId, $exportParams) {
1686 $greetings = [];
1687 $contact = NULL;
1688
1689 $greetingFields = [
1690 'postal_greeting',
1691 'addressee',
1692 ];
1693 foreach ($greetingFields as $greeting) {
1694 if (!empty($exportParams[$greeting])) {
1695 $greetingLabel = $exportParams[$greeting];
1696 if (empty($contact)) {
1697 $values = [
1698 'id' => $contactId,
1699 'version' => 3,
1700 ];
1701 $contact = civicrm_api('contact', 'get', $values);
1702
1703 if (!empty($contact['is_error'])) {
1704 return $greetings;
1705 }
1706 $contact = $contact['values'][$contact['id']];
1707 }
1708
1709 $tokens = ['contact' => $greetingLabel];
1710 $greetings[$greeting] = CRM_Utils_Token::replaceContactTokens($greetingLabel, $contact, NULL, $tokens);
1711 }
1712 }
1713 return $greetings;
1714 }
1715
c8713fd4 1716 /**
1717 * Build array for merging same addresses.
1718 *
1719 * @param $sql
1720 * @param array $exportParams
1721 * @param bool $sharedAddress
1722 *
1723 * @return array
1724 */
1725 public function buildMasterCopyArray($sql, $exportParams, $sharedAddress = FALSE) {
1726 static $contactGreetingTokens = [];
1727
1728 $addresseeOptions = CRM_Core_OptionGroup::values('addressee');
1729 $postalOptions = CRM_Core_OptionGroup::values('postal_greeting');
1730
1731 $merge = $parents = [];
1732 $dao = CRM_Core_DAO::executeQuery($sql);
1733
1734 while ($dao->fetch()) {
1735 $masterID = $dao->master_id;
1736 $copyID = $dao->copy_id;
1737 $masterPostalGreeting = $dao->master_postal_greeting;
1738 $masterAddressee = $dao->master_addressee;
1739 $copyAddressee = $dao->copy_addressee;
1740
1741 if (!$sharedAddress) {
1742 if (!isset($contactGreetingTokens[$dao->master_contact_id])) {
1743 $contactGreetingTokens[$dao->master_contact_id] = $this->replaceMergeTokens($dao->master_contact_id, $exportParams);
1744 }
1745 $masterPostalGreeting = CRM_Utils_Array::value('postal_greeting',
1746 $contactGreetingTokens[$dao->master_contact_id], $dao->master_postal_greeting
1747 );
1748 $masterAddressee = CRM_Utils_Array::value('addressee',
1749 $contactGreetingTokens[$dao->master_contact_id], $dao->master_addressee
1750 );
1751
1752 if (!isset($contactGreetingTokens[$dao->copy_contact_id])) {
1753 $contactGreetingTokens[$dao->copy_contact_id] = $this->replaceMergeTokens($dao->copy_contact_id, $exportParams);
1754 }
1755 $copyPostalGreeting = CRM_Utils_Array::value('postal_greeting',
1756 $contactGreetingTokens[$dao->copy_contact_id], $dao->copy_postal_greeting
1757 );
1758 $copyAddressee = CRM_Utils_Array::value('addressee',
1759 $contactGreetingTokens[$dao->copy_contact_id], $dao->copy_addressee
1760 );
1761 }
1762
1763 if (!isset($merge[$masterID])) {
1764 // check if this is an intermediate child
1765 // this happens if there are 3 or more matches a,b, c
1766 // the above query will return a, b / a, c / b, c
1767 // we might be doing a bit more work, but for now its ok, unless someone
1768 // knows how to fix the query above
1769 if (isset($parents[$masterID])) {
1770 $masterID = $parents[$masterID];
1771 }
1772 else {
1773 $merge[$masterID] = [
1774 'addressee' => $masterAddressee,
1775 'copy' => [],
1776 'postalGreeting' => $masterPostalGreeting,
1777 ];
1778 $merge[$masterID]['emailGreeting'] = &$merge[$masterID]['postalGreeting'];
1779 }
1780 }
1781 $parents[$copyID] = $masterID;
1782
1783 if (!$sharedAddress && !array_key_exists($copyID, $merge[$masterID]['copy'])) {
1784
1785 if (!empty($exportParams['postal_greeting_other']) &&
1786 count($merge[$masterID]['copy']) >= 1
1787 ) {
1788 // use static greetings specified if no of contacts > 2
1789 $merge[$masterID]['postalGreeting'] = $exportParams['postal_greeting_other'];
1790 }
1791 elseif ($copyPostalGreeting) {
1792 $this->trimNonTokensFromAddressString($copyPostalGreeting,
1793 $postalOptions[$dao->copy_postal_greeting_id],
1794 $exportParams
1795 );
1796 $merge[$masterID]['postalGreeting'] = "{$merge[$masterID]['postalGreeting']}, {$copyPostalGreeting}";
1797 // if there happens to be a duplicate, remove it
1798 $merge[$masterID]['postalGreeting'] = str_replace(" {$copyPostalGreeting},", "", $merge[$masterID]['postalGreeting']);
1799 }
1800
1801 if (!empty($exportParams['addressee_other']) &&
1802 count($merge[$masterID]['copy']) >= 1
1803 ) {
1804 // use static greetings specified if no of contacts > 2
1805 $merge[$masterID]['addressee'] = $exportParams['addressee_other'];
1806 }
1807 elseif ($copyAddressee) {
1808 $this->trimNonTokensFromAddressString($copyAddressee,
1809 $addresseeOptions[$dao->copy_addressee_id],
1810 $exportParams, 'addressee'
1811 );
1812 $merge[$masterID]['addressee'] = "{$merge[$masterID]['addressee']}, " . trim($copyAddressee);
1813 }
1814 }
1815 $merge[$masterID]['copy'][$copyID] = $copyAddressee;
1816 }
1817
1818 return $merge;
1819 }
1820
30237bf3 1821 /**
1822 * Merge contacts with the same address.
1823 *
1824 * @param $sqlColumns
1825 * @param array $exportParams
1826 */
1827 public function mergeSameAddress(&$sqlColumns, $exportParams) {
1828 $greetingOptions = CRM_Export_Form_Select::getGreetingOptions();
1829
1830 if (!empty($greetingOptions)) {
1831 // Greeting options is keyed by 'postal_greeting' or 'addressee'.
1832 foreach ($greetingOptions as $key => $value) {
1833 if ($option = CRM_Utils_Array::value($key, $exportParams)) {
1834 if ($greetingOptions[$key][$option] == ts('Other')) {
1835 $exportParams[$key] = $exportParams["{$key}_other"];
1836 }
1837 elseif ($greetingOptions[$key][$option] == ts('List of names')) {
1838 $exportParams[$key] = '';
1839 }
1840 else {
1841 $exportParams[$key] = $greetingOptions[$key][$option];
1842 }
1843 }
1844 }
1845 }
1846 $tableName = $this->getTemporaryTable();
1847 // check if any records are present based on if they have used shared address feature,
1848 // and not based on if city / state .. matches.
1849 $sql = "
1850SELECT r1.id as copy_id,
1851 r1.civicrm_primary_id as copy_contact_id,
1852 r1.addressee as copy_addressee,
1853 r1.addressee_id as copy_addressee_id,
1854 r1.postal_greeting as copy_postal_greeting,
1855 r1.postal_greeting_id as copy_postal_greeting_id,
1856 r2.id as master_id,
1857 r2.civicrm_primary_id as master_contact_id,
1858 r2.postal_greeting as master_postal_greeting,
1859 r2.postal_greeting_id as master_postal_greeting_id,
1860 r2.addressee as master_addressee,
1861 r2.addressee_id as master_addressee_id
1862FROM $tableName r1
1863INNER JOIN civicrm_address adr ON r1.master_id = adr.id
1864INNER JOIN $tableName r2 ON adr.contact_id = r2.civicrm_primary_id
1865ORDER BY r1.id";
1866 $linkedMerge = $this->buildMasterCopyArray($sql, $exportParams, TRUE);
1867
1868 // find all the records that have the same street address BUT not in a household
1869 // require match on city and state as well
1870 $sql = "
1871SELECT r1.id as master_id,
1872 r1.civicrm_primary_id as master_contact_id,
1873 r1.postal_greeting as master_postal_greeting,
1874 r1.postal_greeting_id as master_postal_greeting_id,
1875 r1.addressee as master_addressee,
1876 r1.addressee_id as master_addressee_id,
1877 r2.id as copy_id,
1878 r2.civicrm_primary_id as copy_contact_id,
1879 r2.postal_greeting as copy_postal_greeting,
1880 r2.postal_greeting_id as copy_postal_greeting_id,
1881 r2.addressee as copy_addressee,
1882 r2.addressee_id as copy_addressee_id
1883FROM $tableName r1
1884LEFT JOIN $tableName r2 ON ( r1.street_address = r2.street_address AND
1885 r1.city = r2.city AND
1886 r1.state_province_id = r2.state_province_id )
1887WHERE ( r1.household_name IS NULL OR r1.household_name = '' )
1888AND ( r2.household_name IS NULL OR r2.household_name = '' )
1889AND ( r1.street_address != '' )
1890AND r2.id > r1.id
1891ORDER BY r1.id
1892";
1893 $merge = $this->buildMasterCopyArray($sql, $exportParams);
1894
1895 // unset ids from $merge already present in $linkedMerge
1896 foreach ($linkedMerge as $masterID => $values) {
1897 $keys = [$masterID];
1898 $keys = array_merge($keys, array_keys($values['copy']));
1899 foreach ($merge as $mid => $vals) {
1900 if (in_array($mid, $keys)) {
1901 unset($merge[$mid]);
1902 }
1903 else {
1904 foreach ($values['copy'] as $copyId) {
1905 if (in_array($copyId, $keys)) {
1906 unset($merge[$mid]['copy'][$copyId]);
1907 }
1908 }
1909 }
1910 }
1911 }
1912 $merge = $merge + $linkedMerge;
1913
1914 foreach ($merge as $masterID => $values) {
1915 $sql = "
1916UPDATE $tableName
1917SET addressee = %1, postal_greeting = %2, email_greeting = %3
1918WHERE id = %4
1919";
1920 $params = [
1921 1 => [$values['addressee'], 'String'],
1922 2 => [$values['postalGreeting'], 'String'],
1923 3 => [$values['emailGreeting'], 'String'],
1924 4 => [$masterID, 'Integer'],
1925 ];
1926 CRM_Core_DAO::executeQuery($sql, $params);
1927
1928 // delete all copies
1929 $deleteIDs = array_keys($values['copy']);
1930 $deleteIDString = implode(',', $deleteIDs);
1931 $sql = "
1932DELETE FROM $tableName
1933WHERE id IN ( $deleteIDString )
1934";
1935 CRM_Core_DAO::executeQuery($sql);
1936 }
1937
1938 // unset temporary columns that were added for postal mailing format
1939 // @todo - this part is pretty close to ready to be removed....
1940 if (!empty($exportParams['merge_same_address']['temp_columns'])) {
1941 $unsetKeys = array_keys($sqlColumns);
1942 foreach ($unsetKeys as $headerKey => $sqlColKey) {
1943 if (array_key_exists($sqlColKey, $exportParams['merge_same_address']['temp_columns'])) {
1944 unset($sqlColumns[$sqlColKey]);
1945 }
1946 }
1947 }
1948 }
1949
e358ebd1 1950 /**
1951 * The function unsets static part of the string, if token is the dynamic part.
1952 *
1953 * Example: 'Hello {contact.first_name}' => converted to => '{contact.first_name}'
1954 * i.e 'Hello Alan' => converted to => 'Alan'
1955 *
1956 * @param string $parsedString
1957 * @param string $defaultGreeting
1958 * @param bool $addressMergeGreetings
1959 * @param string $greetingType
1960 *
1961 * @return mixed
1962 */
1963 public function trimNonTokensFromAddressString(
1964 &$parsedString, $defaultGreeting,
1965 $addressMergeGreetings, $greetingType = 'postal_greeting'
1966 ) {
1967 if (!empty($addressMergeGreetings[$greetingType])) {
1968 $greetingLabel = $addressMergeGreetings[$greetingType];
1969 }
1970 $greetingLabel = empty($greetingLabel) ? $defaultGreeting : $greetingLabel;
1971
1972 $stringsToBeReplaced = preg_replace('/(\{[a-zA-Z._ ]+\})/', ';;', $greetingLabel);
1973 $stringsToBeReplaced = explode(';;', $stringsToBeReplaced);
1974 foreach ($stringsToBeReplaced as $key => $string) {
1975 // to keep one space
1976 $stringsToBeReplaced[$key] = ltrim($string);
1977 }
1978 $parsedString = str_replace($stringsToBeReplaced, "", $parsedString);
1979
1980 return $parsedString;
1981 }
1982
6003a964 1983}