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