[test] convert export test to handle exception rather than early return
[civicrm-core.git] / CRM / Export / BAO / ExportProcessor.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
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
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33
34 /**
35 * Class CRM_Export_BAO_ExportProcessor
36 *
37 * Class to handle logic of export.
38 */
39 class CRM_Export_BAO_ExportProcessor {
40
41 /**
42 * @var int
43 */
44 protected $queryMode;
45
46 /**
47 * @var int
48 */
49 protected $exportMode;
50
51 /**
52 * Array of fields in the main query.
53 *
54 * @var array
55 */
56 protected $queryFields = [];
57
58 /**
59 * Either AND or OR.
60 *
61 * @var string
62 */
63 protected $queryOperator;
64
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
75 /**
76 * Is the contact being merged into a single household.
77 *
78 * @var bool
79 */
80 protected $isMergeSameHousehold;
81
82 /**
83 * Only export contacts that can receive postal mail.
84 *
85 * Includes being alive, having an address & not having do_not_mail.
86 *
87 * @var bool
88 */
89 protected $isPostalableOnly;
90
91 /**
92 * Key representing the head of household in the relationship array.
93 *
94 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
95 *
96 * @var array
97 */
98 protected $relationshipTypes = [];
99
100 /**
101 * Array of properties to retrieve for relationships.
102 *
103 * @var array
104 */
105 protected $relationshipReturnProperties = [];
106
107 /**
108 * IDs of households that have already been exported.
109 *
110 * @var array
111 */
112 protected $exportedHouseholds = [];
113
114 /**
115 * Households to skip during export as they will be exported via their relationships anyway.
116 *
117 * @var array
118 */
119 protected $householdsToSkip = [];
120
121 /**
122 * Get return properties by relationship.
123 * @return array
124 */
125 public function getRelationshipReturnProperties() {
126 return $this->relationshipReturnProperties;
127 }
128
129 /**
130 * Export values for related contacts.
131 *
132 * @var array
133 */
134 protected $relatedContactValues = [];
135
136 /**
137 * @var array
138 */
139 protected $returnProperties = [];
140
141 /**
142 * @var array
143 */
144 protected $outputSpecification = [];
145
146 /**
147 * Name of a temporary table created to hold the results.
148 *
149 * Current decision making on when to create a temp table is kinda bad so this might change
150 * a bit as it is reviewed but basically we need a temp table or similar to calculate merging
151 * addresses. Merging households is handled in php. We create a temp table even when we don't need them.
152 *
153 * @var string
154 */
155 protected $temporaryTable;
156
157 /**
158 * @return string
159 */
160 public function getTemporaryTable(): string {
161 return $this->temporaryTable;
162 }
163
164 /**
165 * @param string $temporaryTable
166 */
167 public function setTemporaryTable(string $temporaryTable) {
168 $this->temporaryTable = $temporaryTable;
169 }
170
171 /**
172 * CRM_Export_BAO_ExportProcessor constructor.
173 *
174 * @param int $exportMode
175 * @param array|null $requestedFields
176 * @param string $queryOperator
177 * @param bool $isMergeSameHousehold
178 * @param bool $isPostalableOnly
179 */
180 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE) {
181 $this->setExportMode($exportMode);
182 $this->setQueryMode();
183 $this->setQueryOperator($queryOperator);
184 $this->setRequestedFields($requestedFields);
185 $this->setRelationshipTypes();
186 $this->setIsMergeSameHousehold($isMergeSameHousehold);
187 $this->setisPostalableOnly($isPostalableOnly);
188 }
189
190 /**
191 * @return bool
192 */
193 public function isPostalableOnly() {
194 return $this->isPostalableOnly;
195 }
196
197 /**
198 * @param bool $isPostalableOnly
199 */
200 public function setIsPostalableOnly($isPostalableOnly) {
201 $this->isPostalableOnly = $isPostalableOnly;
202 }
203
204 /**
205 * @return array|null
206 */
207 public function getRequestedFields() {
208 return $this->requestedFields;
209 }
210
211 /**
212 * @param array|null $requestedFields
213 */
214 public function setRequestedFields($requestedFields) {
215 $this->requestedFields = $requestedFields;
216 }
217
218 /**
219 * @return array
220 */
221 public function getReturnProperties() {
222 return $this->returnProperties;
223 }
224
225 /**
226 * @param array $returnProperties
227 */
228 public function setReturnProperties($returnProperties) {
229 $this->returnProperties = $returnProperties;
230 }
231
232 /**
233 * @return array
234 */
235 public function getRelationshipTypes() {
236 return $this->relationshipTypes;
237 }
238
239 /**
240 */
241 public function setRelationshipTypes() {
242 $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
243 NULL,
244 NULL,
245 NULL,
246 NULL,
247 TRUE,
248 'name',
249 FALSE
250 );
251 }
252
253 /**
254 * Set the value for a relationship type field.
255 *
256 * In this case we are building up an array of properties for a related contact.
257 *
258 * These may be used for direct exporting or for merge to household depending on the
259 * options selected.
260 *
261 * @param string $relationshipType
262 * @param int $contactID
263 * @param string $field
264 * @param string $value
265 */
266 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
267 $this->relatedContactValues[$relationshipType][$contactID][$field] = $value;
268 if ($field === 'id') {
269 $this->householdsToSkip[] = $value;
270 }
271 }
272
273 /**
274 * Get the value for a relationship type field.
275 *
276 * In this case we are building up an array of properties for a related contact.
277 *
278 * These may be used for direct exporting or for merge to household depending on the
279 * options selected.
280 *
281 * @param string $relationshipType
282 * @param int $contactID
283 * @param string $field
284 *
285 * @return string
286 */
287 public function getRelationshipValue($relationshipType, $contactID, $field) {
288 return isset($this->relatedContactValues[$relationshipType][$contactID][$field]) ? $this->relatedContactValues[$relationshipType][$contactID][$field] : '';
289 }
290
291 /**
292 * Get the id of the related household.
293 *
294 * @param int $contactID
295 * @param string $relationshipType
296 *
297 * @return int
298 */
299 public function getRelatedHouseholdID($contactID, $relationshipType) {
300 return $this->relatedContactValues[$relationshipType][$contactID]['id'];
301 }
302
303 /**
304 * Has the household already been exported.
305 *
306 * @param int $housholdContactID
307 *
308 * @return bool
309 */
310 public function isHouseholdExported($housholdContactID) {
311 return isset($this->exportedHouseholds[$housholdContactID]);
312
313 }
314
315 /**
316 * @return bool
317 */
318 public function isMergeSameHousehold() {
319 return $this->isMergeSameHousehold;
320 }
321
322 /**
323 * @param bool $isMergeSameHousehold
324 */
325 public function setIsMergeSameHousehold($isMergeSameHousehold) {
326 $this->isMergeSameHousehold = $isMergeSameHousehold;
327 }
328
329 /**
330 * Return relationship types for household merge.
331 *
332 * @return mixed
333 */
334 public function getHouseholdRelationshipTypes() {
335 if (!$this->isMergeSameHousehold()) {
336 return [];
337 }
338 return [
339 CRM_Utils_Array::key('Household Member of', $this->getRelationshipTypes()),
340 CRM_Utils_Array::key('Head of Household for', $this->getRelationshipTypes()),
341 ];
342 }
343
344 /**
345 * @param $fieldName
346 * @return bool
347 */
348 public function isRelationshipTypeKey($fieldName) {
349 return array_key_exists($fieldName, $this->relationshipTypes);
350 }
351
352 /**
353 * @param $fieldName
354 * @return bool
355 */
356 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
357 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
358 }
359
360 /**
361 * @return string
362 */
363 public function getQueryOperator() {
364 return $this->queryOperator;
365 }
366
367 /**
368 * @param string $queryOperator
369 */
370 public function setQueryOperator($queryOperator) {
371 $this->queryOperator = $queryOperator;
372 }
373
374 /**
375 * @return array
376 */
377 public function getQueryFields() {
378 return $this->queryFields;
379 }
380
381 /**
382 * @param array $queryFields
383 */
384 public function setQueryFields($queryFields) {
385 // legacy hacks - we add these to queryFields because this
386 // pseudometadata is currently required.
387 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
388 $queryFields['country']['context'] = 'country';
389 $queryFields['world_region']['context'] = 'country';
390 $queryFields['state_province']['context'] = 'province';
391 $this->queryFields = $queryFields;
392 }
393
394 /**
395 * @return int
396 */
397 public function getQueryMode() {
398 return $this->queryMode;
399 }
400
401 /**
402 * Set the query mode based on the export mode.
403 */
404 public function setQueryMode() {
405
406 switch ($this->getExportMode()) {
407 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
408 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE;
409 break;
410
411 case CRM_Export_Form_Select::EVENT_EXPORT:
412 $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT;
413 break;
414
415 case CRM_Export_Form_Select::MEMBER_EXPORT:
416 $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER;
417 break;
418
419 case CRM_Export_Form_Select::PLEDGE_EXPORT:
420 $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE;
421 break;
422
423 case CRM_Export_Form_Select::CASE_EXPORT:
424 $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE;
425 break;
426
427 case CRM_Export_Form_Select::GRANT_EXPORT:
428 $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT;
429 break;
430
431 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
432 $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY;
433 break;
434
435 default:
436 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS;
437 }
438 }
439
440 /**
441 * @return int
442 */
443 public function getExportMode() {
444 return $this->exportMode;
445 }
446
447 /**
448 * @param int $exportMode
449 */
450 public function setExportMode($exportMode) {
451 $this->exportMode = $exportMode;
452 }
453
454 /**
455 * Get the name for the export file.
456 *
457 * @return string
458 */
459 public function getExportFileName() {
460 switch ($this->getExportMode()) {
461 case CRM_Export_Form_Select::CONTACT_EXPORT:
462 return ts('CiviCRM Contact Search');
463
464 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
465 return ts('CiviCRM Contribution Search');
466
467 case CRM_Export_Form_Select::MEMBER_EXPORT:
468 return ts('CiviCRM Member Search');
469
470 case CRM_Export_Form_Select::EVENT_EXPORT:
471 return ts('CiviCRM Participant Search');
472
473 case CRM_Export_Form_Select::PLEDGE_EXPORT:
474 return ts('CiviCRM Pledge Search');
475
476 case CRM_Export_Form_Select::CASE_EXPORT:
477 return ts('CiviCRM Case Search');
478
479 case CRM_Export_Form_Select::GRANT_EXPORT:
480 return ts('CiviCRM Grant Search');
481
482 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
483 return ts('CiviCRM Activity Search');
484
485 default:
486 // Legacy code suggests the value could be 'financial' - ie. something
487 // other than what should be accepted. However, I suspect that this line is
488 // never hit.
489 return ts('CiviCRM Search');
490 }
491 }
492
493 /**
494 * Get the label for the header row based on the field to output.
495 *
496 * @param string $field
497 *
498 * @return string
499 */
500 public function getHeaderForRow($field) {
501 if (substr($field, -11) == 'campaign_id') {
502 // @todo - set this correctly in the xml rather than here.
503 // This will require a generalised handling cleanup
504 return ts('Campaign ID');
505 }
506 if ($this->isMergeSameHousehold() && $field === 'id') {
507 return ts('Household ID');
508 }
509 elseif (isset($this->getQueryFields()[$field]['title'])) {
510 return $this->getQueryFields()[$field]['title'];
511 }
512 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
513 return CRM_Utils_Array::value($field, $this->getcomponentPaymentFields());
514 }
515 else {
516 return $field;
517 }
518 }
519
520 /**
521 * @param $params
522 * @param $order
523 * @param $returnProperties
524 * @return array
525 */
526 public function runQuery($params, $order, $returnProperties) {
527 $addressWhere = '';
528 $params = array_merge($params, $this->getWhereParams());
529 if ($this->isPostalableOnly) {
530 if (array_key_exists('street_address', $returnProperties)) {
531 $addressWhere = " civicrm_address.street_address <> ''";
532 if (array_key_exists('supplemental_address_1', $returnProperties)) {
533 // We need this to be an OR rather than AND on the street_address so, hack it in.
534 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
535 'address_options', TRUE, NULL, TRUE
536 );
537 if (!empty($addressOptions['supplemental_address_1'])) {
538 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
539 }
540 }
541 $addressWhere = ' AND (' . $addressWhere . ')';
542 }
543 }
544 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
545 FALSE, FALSE, $this->getQueryMode(),
546 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
547 );
548
549 //sort by state
550 //CRM-15301
551 $query->_sort = $order;
552 list($select, $from, $where, $having) = $query->query();
553 $this->setQueryFields($query->_fields);
554 return [$query, $select, $from, $where . $addressWhere, $having];
555 }
556
557 /**
558 * Add a row to the specification for how to output data.
559 *
560 * @param string $key
561 * @param string $relationshipType
562 * @param string $locationType
563 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
564 */
565 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
566 $entityLabel = '';
567 if ($entityTypeID) {
568 if ($key === 'phone') {
569 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
570 }
571 if ($key === 'im') {
572 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
573 }
574 }
575
576 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
577 // but in time it may be good to rationalise them.
578 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
579 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
580 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
581
582 $this->outputSpecification[$index]['header'] = $label;
583 $this->outputSpecification[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
584
585 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
586 $this->setColumnAsCalculationOnly($index);
587 }
588 $this->outputSpecification[$index]['metadata'] = $this->getMetaDataForField($key);
589 }
590
591 /**
592 * Get the metadata for the given field.
593 *
594 * @param $key
595 *
596 * @return array
597 */
598 public function getMetaDataForField($key) {
599 $mappings = ['contact_id' => 'id'];
600 if (isset($this->getQueryFields()[$key])) {
601 return $this->getQueryFields()[$key];
602 }
603 if (isset($mappings[$key])) {
604 return $this->getQueryFields()[$mappings[$key]];
605 }
606 return [];
607 }
608
609 /**
610 * @param $key
611 */
612 public function setSqlColumnDefn($key) {
613 $this->outputSpecification[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
614 }
615
616 /**
617 * Mark a column as only required for calculations.
618 *
619 * Do not include the row with headers.
620 *
621 * @param string $column
622 */
623 public function setColumnAsCalculationOnly($column) {
624 $this->outputSpecification[$column]['do_not_output_to_csv'] = TRUE;
625 }
626
627 /**
628 * @return array
629 */
630 public function getHeaderRows() {
631 $headerRows = [];
632 foreach ($this->outputSpecification as $key => $spec) {
633 if (empty($spec['do_not_output_to_csv'])) {
634 $headerRows[] = $spec['header'];
635 }
636 }
637 return $headerRows;
638 }
639
640 /**
641 * @return array
642 */
643 public function getSQLColumns() {
644 $sqlColumns = [];
645 foreach ($this->outputSpecification as $key => $spec) {
646 if (empty($spec['do_not_output_to_sql'])) {
647 $sqlColumns[$key] = $spec['sql_columns'];
648 }
649 }
650 return $sqlColumns;
651 }
652
653 /**
654 * @return array
655 */
656 public function getMetadata() {
657 $metadata = [];
658 foreach ($this->outputSpecification as $key => $spec) {
659 $metadata[$key] = $spec['metadata'];
660 }
661 return $metadata;
662 }
663
664 /**
665 * Build the row for output.
666 *
667 * @param \CRM_Contact_BAO_Query $query
668 * @param CRM_Core_DAO $iterationDAO
669 * @param array $outputColumns
670 * @param $metadata
671 * @param $paymentDetails
672 * @param $addPaymentHeader
673 * @param $paymentTableId
674 *
675 * @return array|bool
676 */
677 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader, $paymentTableId) {
678 if ($this->isHouseholdToSkip($iterationDAO->contact_id)) {
679 return FALSE;
680 }
681 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
682 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
683
684 $row = [];
685 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id);
686 if ($householdMergeRelationshipType) {
687 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id, $householdMergeRelationshipType);
688 if ($this->isHouseholdExported($householdID)) {
689 return FALSE;
690 }
691 foreach (array_keys($outputColumns) as $column) {
692 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id, $column);
693 }
694 $this->markHouseholdExported($householdID);
695 return $row;
696 }
697
698 $query->convertToPseudoNames($iterationDAO);
699
700 //first loop through output columns so that we return what is required, and in same order.
701 foreach ($outputColumns as $field => $value) {
702 // add im_provider to $dao object
703 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
704 $iterationDAO->im_provider = $iterationDAO->provider_id;
705 }
706
707 //build row values (data)
708 $fieldValue = NULL;
709 if (property_exists($iterationDAO, $field)) {
710 $fieldValue = $iterationDAO->$field;
711 // to get phone type from phone type id
712 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
713 $fieldValue = $phoneTypes[$fieldValue];
714 }
715 elseif ($field == 'provider_id' || $field == 'im_provider') {
716 $fieldValue = CRM_Utils_Array::value($fieldValue, $imProviders);
717 }
718 elseif (strstr($field, 'master_id')) {
719 $masterAddressId = NULL;
720 if (isset($iterationDAO->$field)) {
721 $masterAddressId = $iterationDAO->$field;
722 }
723 // get display name of contact that address is shared.
724 $fieldValue = CRM_Contact_BAO_Contact::getMasterDisplayName($masterAddressId);
725 }
726 }
727
728 if ($this->isRelationshipTypeKey($field)) {
729 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id, $value, $field);
730 }
731 else {
732 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
733 }
734 }
735
736 // If specific payment fields have been selected for export, payment
737 // data will already be in $row. Otherwise, add payment related
738 // information, if appropriate.
739 if ($addPaymentHeader) {
740 if (!$this->isExportSpecifiedPaymentFields()) {
741 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
742 if ($this->isExportPaymentFields()) {
743 $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails);
744 if (!is_array($paymentData) || empty($paymentData)) {
745 $paymentData = $nullContributionDetails;
746 }
747 $row = array_merge($row, $paymentData);
748 }
749 elseif (!empty($paymentDetails)) {
750 $row = array_merge($row, $nullContributionDetails);
751 }
752 }
753 }
754 //remove organization name for individuals if it is set for current employer
755 if (!empty($row['contact_type']) &&
756 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
757 ) {
758 $row['organization_name'] = '';
759 }
760 return $row;
761 }
762
763 /**
764 * If this row has a household whose details we should use get the relationship type key.
765 *
766 * @param $contactID
767 *
768 * @return bool
769 */
770 public function getHouseholdMergeTypeForRow($contactID) {
771 if (!$this->isMergeSameHousehold()) {
772 return FALSE;
773 }
774 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
775 if (isset($this->relatedContactValues[$relationshipType][$contactID])) {
776 return $relationshipType;
777 }
778 }
779 }
780
781 /**
782 * Mark the given household as already exported.
783 *
784 * @param $householdID
785 */
786 public function markHouseholdExported($householdID) {
787 $this->exportedHouseholds[$householdID] = $householdID;
788 }
789
790 /**
791 * @param $field
792 * @param $iterationDAO
793 * @param $fieldValue
794 * @param $metadata
795 * @param $paymentDetails
796 *
797 * @return string
798 */
799 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
800
801 $i18n = CRM_Core_I18n::singleton();
802 if ($field == 'id') {
803 return $iterationDAO->contact_id;
804 // special case for calculated field
805 }
806 elseif ($field == 'source_contact_id') {
807 return $iterationDAO->contact_id;
808 }
809 elseif ($field == 'pledge_balance_amount') {
810 return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid;
811 // special case for calculated field
812 }
813 elseif ($field == 'pledge_next_pay_amount') {
814 return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount;
815 }
816 elseif (isset($fieldValue) &&
817 $fieldValue != ''
818 ) {
819 //check for custom data
820 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) {
821 return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
822 }
823
824 elseif (in_array($field, [
825 'email_greeting',
826 'postal_greeting',
827 'addressee',
828 ])) {
829 //special case for greeting replacement
830 $fldValue = "{$field}_display";
831 return $iterationDAO->$fldValue;
832 }
833 else {
834 //normal fields with a touch of CRM-3157
835 switch ($field) {
836 case 'country':
837 case 'world_region':
838 return $i18n->crm_translate($fieldValue, ['context' => 'country']);
839
840 case 'state_province':
841 return $i18n->crm_translate($fieldValue, ['context' => 'province']);
842
843 case 'gender':
844 case 'preferred_communication_method':
845 case 'preferred_mail_format':
846 case 'communication_style':
847 return $i18n->crm_translate($fieldValue);
848
849 default:
850 if (isset($metadata[$field])) {
851 // No I don't know why we do it this way & whether we could
852 // make better use of pseudoConstants.
853 if (!empty($metadata[$field]['context'])) {
854 return $i18n->crm_translate($fieldValue, $metadata[$field]);
855 }
856 if (!empty($metadata[$field]['pseudoconstant'])) {
857 if (!empty($metadata[$field]['bao'])) {
858 return CRM_Core_PseudoConstant::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
859 }
860 // This is not our normal syntax for pseudoconstants but I am a bit loath to
861 // call an external function until sure it is not increasing php processing given this
862 // may be iterated 100,000 times & we already have the $imProvider var loaded.
863 // That can be next refactor...
864 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
865 // but I have to reach a stable point....
866 $varName = $metadata[$field]['pseudoconstant']['var'];
867 if ($varName === 'imProviders') {
868 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
869 }
870 if ($varName === 'phoneTypes') {
871 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
872 }
873 }
874
875 }
876 return $fieldValue;
877 }
878 }
879 }
880 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
881 $paymentTableId = $this->getPaymentTableID();
882 $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails);
883 $payFieldMapper = [
884 'componentPaymentField_total_amount' => 'total_amount',
885 'componentPaymentField_contribution_status' => 'contribution_status',
886 'componentPaymentField_payment_instrument' => 'pay_instru',
887 'componentPaymentField_transaction_id' => 'trxn_id',
888 'componentPaymentField_received_date' => 'receive_date',
889 ];
890 return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, '');
891 }
892 else {
893 // if field is empty or null
894 return '';
895 }
896 }
897
898 /**
899 * Get array of fields to return, over & above those defined in the main contact exportable fields.
900 *
901 * These include export mode specific fields & some fields apparently required as 'exportableFields'
902 * but not returned by the function of the same name.
903 *
904 * @return array
905 * Array of fields to return in the format ['field_name' => 1,...]
906 */
907 public function getAdditionalReturnProperties() {
908 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) {
909 $componentSpecificFields = [];
910 }
911 else {
912 $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode());
913 }
914 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) {
915 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode()));
916 unset($componentSpecificFields['contribution_status_id']);
917 unset($componentSpecificFields['pledge_status_id']);
918 unset($componentSpecificFields['pledge_payment_status_id']);
919 }
920 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) {
921 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode()));
922 }
923 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) {
924 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE));
925 unset($componentSpecificFields['contribution_status_id']);
926 }
927 return $componentSpecificFields;
928 }
929
930 /**
931 * Should payment fields be appended to the export.
932 *
933 * (This is pretty hacky so hopefully this function won't last long - notice
934 * how obviously it should be part of the above function!).
935 */
936 public function isExportPaymentFields() {
937 if ($this->getRequestedFields() === NULL
938 && in_array($this->getQueryMode(), [
939 CRM_Contact_BAO_Query::MODE_EVENT,
940 CRM_Contact_BAO_Query::MODE_MEMBER,
941 CRM_Contact_BAO_Query::MODE_PLEDGE,
942 ])) {
943 return TRUE;
944 }
945 elseif ($this->isExportSpecifiedPaymentFields()) {
946 return TRUE;
947 }
948 return FALSE;
949 }
950
951 /**
952 * Has specific payment fields been requested (as opposed to via all fields).
953 *
954 * If specific fields have been requested then they get added at various points.
955 *
956 * @return bool
957 */
958 public function isExportSpecifiedPaymentFields() {
959 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
960 return TRUE;
961 }
962 }
963
964 /**
965 * Get the name of the id field in the table that connects contributions to the export entity.
966 */
967 public function getPaymentTableID() {
968 if ($this->getRequestedFields() === NULL) {
969 $mapping = [
970 CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id',
971 CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id',
972 CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id',
973 ];
974 return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : '';
975 }
976 elseif ($this->hasRequestedComponentPaymentFields()) {
977 return 'participant_id';
978 }
979 return FALSE;
980 }
981
982 /**
983 * Have component payment fields been requested.
984 *
985 * @return bool
986 */
987 protected function hasRequestedComponentPaymentFields() {
988 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) {
989 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
990 if (!empty($participantPaymentFields)) {
991 return TRUE;
992 }
993 }
994 return FALSE;
995 }
996
997 /**
998 * Get fields that indicate payment fields have been requested for a component.
999 *
1000 * Ideally this should be protected but making it temporarily public helps refactoring..
1001 *
1002 * @return array
1003 */
1004 public function getComponentPaymentFields() {
1005 return [
1006 'componentPaymentField_total_amount' => ts('Total Amount'),
1007 'componentPaymentField_contribution_status' => ts('Contribution Status'),
1008 'componentPaymentField_received_date' => ts('Date Received'),
1009 'componentPaymentField_payment_instrument' => ts('Payment Method'),
1010 'componentPaymentField_transaction_id' => ts('Transaction ID'),
1011 ];
1012 }
1013
1014 /**
1015 * Get headers for payment fields.
1016 *
1017 * Returns an array of contribution fields when the entity supports payment fields and specific fields
1018 * are not specified. This is a transitional function for refactoring legacy code.
1019 */
1020 public function getPaymentHeaders() {
1021 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
1022 return $this->getcomponentPaymentFields();
1023 }
1024 return [];
1025 }
1026
1027 /**
1028 * Get the default properties when not specified.
1029 *
1030 * In the UI this appears as 'Primary fields only' but in practice it's
1031 * most of the kitchen sink and the hallway closet thrown in.
1032 *
1033 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
1034 *
1035 * @return array
1036 */
1037 public function getDefaultReturnProperties() {
1038 $returnProperties = [];
1039 $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE);
1040 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [
1041 'groups',
1042 'tags',
1043 'notes',
1044 ];
1045
1046 foreach ($fields as $key => $var) {
1047 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1048 $returnProperties[$key] = 1;
1049 }
1050 }
1051 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1052 return $returnProperties;
1053 }
1054
1055 /**
1056 * Add the field to relationship return properties & return it.
1057 *
1058 * This function is doing both setting & getting which is yuck but it is an interim
1059 * refactor.
1060 *
1061 * @param array $value
1062 * @param string $relationshipKey
1063 *
1064 * @return array
1065 */
1066 public function setRelationshipReturnProperties($value, $relationshipKey) {
1067 $relPhoneTypeId = $relIMProviderId = NULL;
1068 if (!empty($value[2])) {
1069 $relationField = CRM_Utils_Array::value(2, $value);
1070 if (trim(CRM_Utils_Array::value(3, $value))) {
1071 $relLocTypeId = CRM_Utils_Array::value(3, $value);
1072 }
1073 else {
1074 $relLocTypeId = 'Primary';
1075 }
1076
1077 if ($relationField == 'phone') {
1078 $relPhoneTypeId = CRM_Utils_Array::value(4, $value);
1079 }
1080 elseif ($relationField == 'im') {
1081 $relIMProviderId = CRM_Utils_Array::value(4, $value);
1082 }
1083 }
1084 elseif (!empty($value[4])) {
1085 $relationField = CRM_Utils_Array::value(4, $value);
1086 $relLocTypeId = CRM_Utils_Array::value(5, $value);
1087 if ($relationField == 'phone') {
1088 $relPhoneTypeId = CRM_Utils_Array::value(6, $value);
1089 }
1090 elseif ($relationField == 'im') {
1091 $relIMProviderId = CRM_Utils_Array::value(6, $value);
1092 }
1093 }
1094 if (in_array($relationField, $this->getValidLocationFields()) && is_numeric($relLocTypeId)) {
1095 $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
1096 if ($relPhoneTypeId) {
1097 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
1098 }
1099 elseif ($relIMProviderId) {
1100 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
1101 }
1102 else {
1103 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1;
1104 }
1105 }
1106 else {
1107 $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1;
1108 }
1109 return $this->relationshipReturnProperties[$relationshipKey];
1110 }
1111
1112 /**
1113 * Add the main return properties to the household merge properties if needed for merging.
1114 *
1115 * If we are using household merge we need to add these to the relationship properties to
1116 * be retrieved.
1117 *
1118 * @param $returnProperties
1119 */
1120 public function setHouseholdMergeReturnProperties($returnProperties) {
1121 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1122 $this->relationshipReturnProperties[$householdRelationshipType] = $returnProperties;
1123 }
1124 }
1125
1126 /**
1127 * Get the default location fields to request.
1128 *
1129 * @return array
1130 */
1131 public function getValidLocationFields() {
1132 return [
1133 'street_address',
1134 'supplemental_address_1',
1135 'supplemental_address_2',
1136 'supplemental_address_3',
1137 'city',
1138 'postal_code',
1139 'postal_code_suffix',
1140 'geo_code_1',
1141 'geo_code_2',
1142 'state_province',
1143 'country',
1144 'phone',
1145 'email',
1146 'im',
1147 ];
1148 }
1149
1150 /**
1151 * Get the sql column definition for the given field.
1152 *
1153 * @param string $fieldName
1154 * @param string $columnName
1155 *
1156 * @return mixed
1157 */
1158 public function getSqlColumnDefinition($fieldName, $columnName) {
1159
1160 // early exit for master_id, CRM-12100
1161 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1162 // also for current_employer, CRM-16939
1163 if ($columnName == 'master_id' || $columnName == 'current_employer') {
1164 return "$fieldName varchar(128)";
1165 }
1166
1167 if (substr($fieldName, -11) == 'campaign_id') {
1168 // CRM-14398
1169 return "$fieldName varchar(128)";
1170 }
1171
1172 $queryFields = $this->getQueryFields();
1173 $lookUp = ['prefix_id', 'suffix_id'];
1174 // set the sql columns
1175 if (isset($queryFields[$columnName]['type'])) {
1176 switch ($queryFields[$columnName]['type']) {
1177 case CRM_Utils_Type::T_INT:
1178 case CRM_Utils_Type::T_BOOLEAN:
1179 if (in_array($columnName, $lookUp)) {
1180 return "$fieldName varchar(255)";
1181 }
1182 else {
1183 return "$fieldName varchar(16)";
1184 }
1185
1186 case CRM_Utils_Type::T_STRING:
1187 if (isset($queryFields[$columnName]['maxlength'])) {
1188 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
1189 }
1190 else {
1191 return "$fieldName varchar(255)";
1192 }
1193
1194 case CRM_Utils_Type::T_TEXT:
1195 case CRM_Utils_Type::T_LONGTEXT:
1196 case CRM_Utils_Type::T_BLOB:
1197 case CRM_Utils_Type::T_MEDIUMBLOB:
1198 return "$fieldName longtext";
1199
1200 case CRM_Utils_Type::T_FLOAT:
1201 case CRM_Utils_Type::T_ENUM:
1202 case CRM_Utils_Type::T_DATE:
1203 case CRM_Utils_Type::T_TIME:
1204 case CRM_Utils_Type::T_TIMESTAMP:
1205 case CRM_Utils_Type::T_MONEY:
1206 case CRM_Utils_Type::T_EMAIL:
1207 case CRM_Utils_Type::T_URL:
1208 case CRM_Utils_Type::T_CCNUM:
1209 default:
1210 return "$fieldName varchar(32)";
1211 }
1212 }
1213 else {
1214 if (substr($fieldName, -3, 3) == '_id') {
1215 return "$fieldName varchar(255)";
1216 }
1217 elseif (substr($fieldName, -5, 5) == '_note') {
1218 return "$fieldName text";
1219 }
1220 else {
1221 $changeFields = [
1222 'groups',
1223 'tags',
1224 'notes',
1225 ];
1226
1227 if (in_array($fieldName, $changeFields)) {
1228 return "$fieldName text";
1229 }
1230 else {
1231 // set the sql columns for custom data
1232 if (isset($queryFields[$columnName]['data_type'])) {
1233
1234 switch ($queryFields[$columnName]['data_type']) {
1235 case 'String':
1236 // May be option labels, which could be up to 512 characters
1237 $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$columnName]));
1238 return "$fieldName varchar($length)";
1239
1240 case 'Country':
1241 case 'StateProvince':
1242 case 'Link':
1243 return "$fieldName varchar(255)";
1244
1245 case 'Memo':
1246 return "$fieldName text";
1247
1248 default:
1249 return "$fieldName varchar(255)";
1250 }
1251 }
1252 else {
1253 return "$fieldName text";
1254 }
1255 }
1256 }
1257 }
1258 }
1259
1260 /**
1261 * Get the munged field name.
1262 *
1263 * @param string $field
1264 * @return string
1265 */
1266 public function getMungedFieldName($field) {
1267 $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64);
1268 if ($fieldName == 'id') {
1269 $fieldName = 'civicrm_primary_id';
1270 }
1271 return $fieldName;
1272 }
1273
1274 /**
1275 * In order to respect the history of this class we need to index kinda illogically.
1276 *
1277 * On the bright side - this stuff is tested within a nano-byte of it's life.
1278 *
1279 * e.g '2-a-b_Home-City'
1280 *
1281 * @param string $key
1282 * @param string $relationshipType
1283 * @param string $locationType
1284 * @param $entityLabel
1285 *
1286 * @return string
1287 */
1288 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
1289 if ($entityLabel || $key === 'im') {
1290 // Just cos that's the history...
1291 if ($key !== 'master_id') {
1292 $key = $this->getHeaderForRow($key);
1293 }
1294 }
1295 if (!$relationshipType || $key !== 'id') {
1296 $key = $this->getMungedFieldName($key);
1297 }
1298 return $this->getMungedFieldName(
1299 ($relationshipType ? ($relationshipType . '_') : '')
1300 . ($locationType ? ($locationType . '_') : '')
1301 . $key
1302 . ($entityLabel ? ('_' . $entityLabel) : '')
1303 );
1304 }
1305
1306 /**
1307 * Get the compiled label for the column.
1308 *
1309 * e.g 'Gender', 'Employee Of-Home-city'
1310 *
1311 * @param string $key
1312 * @param string $relationshipType
1313 * @param string $locationType
1314 * @param string $entityLabel
1315 *
1316 * @return string
1317 */
1318 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1319 return ($relationshipType ? $this->getRelationshipTypes()[$relationshipType] . '-' : '')
1320 . ($locationType ? $locationType . '-' : '')
1321 . $this->getHeaderForRow($key)
1322 . ($entityLabel ? '-' . $entityLabel : '');
1323 }
1324
1325 /**
1326 * Get the mysql field name key.
1327 *
1328 * This key is locked in by tests but the reasons for the specific conventions -
1329 * ie. headings are used for keying fields in some cases, are likely
1330 * accidental rather than deliberate.
1331 *
1332 * This key is used for the output sql array.
1333 *
1334 * @param string $key
1335 * @param $relationshipType
1336 * @param $locationType
1337 * @param $entityLabel
1338 *
1339 * @return string
1340 */
1341 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
1342 if ($entityLabel || $key === 'im') {
1343 if ($key !== 'state_province' && $key !== 'id') {
1344 // @todo - test removing this - indexing by $key should be fine...
1345 $key = $this->getHeaderForRow($key);
1346 }
1347 }
1348 if (!$relationshipType || $key !== 'id') {
1349 $key = $this->getMungedFieldName($key);
1350 }
1351 $fieldKey = $this->getMungedFieldName(
1352 ($relationshipType ? ($relationshipType . '_') : '')
1353 . ($locationType ? ($locationType . '_') : '')
1354 . $key
1355 . ($entityLabel ? ('_' . $entityLabel) : '')
1356 );
1357 return $fieldKey;
1358 }
1359
1360 /**
1361 * Get params for the where criteria.
1362 *
1363 * @return mixed
1364 */
1365 public function getWhereParams() {
1366 if (!$this->isPostalableOnly()) {
1367 return [];
1368 }
1369 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1370 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1371 return $params;
1372 }
1373
1374 /**
1375 * @param $row
1376 * @param $contactID
1377 * @param $value
1378 * @param $field
1379 */
1380 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1381 foreach (array_keys($value) as $property) {
1382 if ($property === 'location') {
1383 // @todo just undo all this nasty location wrangling!
1384 foreach ($value['location'] as $locationKey => $locationFields) {
1385 foreach (array_keys($locationFields) as $locationField) {
1386 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1387 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1388 }
1389 }
1390 }
1391 else {
1392 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1393 }
1394 }
1395 }
1396
1397 /**
1398 * Is this contact a household that is already set to be exported by virtue of it's household members.
1399 *
1400 * @param int $contactID
1401 *
1402 * @return bool
1403 */
1404 protected function isHouseholdToSkip($contactID) {
1405 return in_array($contactID, $this->householdsToSkip);
1406 }
1407
1408 }