Merge pull request #13689 from eileenmcnaughton/no_record_payment
[civicrm-core.git] / CRM / Contact / Form / Task.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
32 */
33
34/**
5a409b50 35 * This class generates form components for search-result tasks.
6a488035 36 */
31aaf096 37class CRM_Contact_Form_Task extends CRM_Core_Form_Task {
6a488035
TO
38
39 /**
100fef9d 40 * The task being performed
6a488035
TO
41 *
42 * @var int
43 */
44 protected $_task;
45
46 /**
47 * The array that holds all the contact ids
48 *
49 * @var array
50 */
51 public $_contactIds;
52
53 /**
54 * The array that holds all the contact types
55 *
56 * @var array
57 */
58 public $_contactTypes;
59
60 /**
61 * The additional clause that we restrict the search with
62 *
63 * @var string
64 */
65 protected $_componentClause = NULL;
66
67 /**
68 * The name of the temp table where we store the contact IDs
69 *
70 * @var string
71 */
72 protected $_componentTable = NULL;
73
74 /**
75 * The array that holds all the component ids
76 *
77 * @var array
78 */
79 protected $_componentIds;
80
81 /**
82 * This includes the submitted values of the search form
69078420 83 * @var array
6a488035
TO
84 */
85 static protected $_searchFormValues;
86
87 /**
d8689418 88 * Build all the data structures needed to build the form.
8ef12e64 89 */
00be9182 90 public function preProcess() {
6a488035
TO
91 self::preProcessCommon($this);
92 }
93
86538308 94 /**
d8689418
EM
95 * Common pre-processing function.
96 *
c490a46a 97 * @param CRM_Core_Form $form
a0174743
MW
98 *
99 * @throws \CRM_Core_Exception
86538308 100 */
2b089ce1 101 public static function preProcessCommon(&$form) {
be2fb01f
CW
102 $form->_contactIds = [];
103 $form->_contactTypes = [];
6a488035 104
fe9b66c1 105 $useTable = (CRM_Utils_System::getClassName($form->controller->getStateMachine()) == 'CRM_Export_StateMachine_Standalone');
2b089ce1 106
78b22ce9 107 $isStandAlone = in_array('task', $form->urlPath) || in_array('standalone', $form->urlPath);
061ff68c 108 if ($isStandAlone) {
109 list($form->_task, $title) = CRM_Contact_Task::getTaskAndTitleByClass(get_class($form));
110 if (!array_key_exists($form->_task, CRM_Contact_Task::permissionedTaskTitles(CRM_Core_Permission::getPermission()))) {
f2774867 111 CRM_Core_Error::statusBounce(ts('You do not have permission to access this page.'));
061ff68c 112 }
78b22ce9 113 $form->_contactIds = explode(',', CRM_Utils_Request::retrieve('cids', 'CommaSeparatedIntegers', $form, TRUE));
061ff68c 114 if (empty($form->_contactIds)) {
6bc234fa 115 CRM_Core_Error::statusBounce(ts('No Contacts Selected'));
061ff68c 116 }
117 $form->setTitle($title);
118 }
119
6a488035
TO
120 // get the submitted values of the search form
121 // we'll need to get fv from either search or adv search in the future
122 $fragment = 'search';
123 if ($form->_action == CRM_Core_Action::ADVANCED) {
124 self::$_searchFormValues = $form->controller->exportValues('Advanced');
125 $fragment .= '/advanced';
126 }
127 elseif ($form->_action == CRM_Core_Action::PROFILE) {
128 self::$_searchFormValues = $form->controller->exportValues('Builder');
129 $fragment .= '/builder';
130 }
131 elseif ($form->_action == CRM_Core_Action::COPY) {
132 self::$_searchFormValues = $form->controller->exportValues('Custom');
133 $fragment .= '/custom';
134 }
061ff68c 135 elseif (!$isStandAlone) {
6a488035
TO
136 self::$_searchFormValues = $form->controller->exportValues('Basic');
137 }
138
139 //set the user context for redirection of task actions
140 $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $form);
141 $urlParams = 'force=1';
142 if (CRM_Utils_Rule::qfKey($qfKey)) {
143 $urlParams .= "&qfKey=$qfKey";
144 }
145
146 $cacheKey = "civicrm search {$qfKey}";
147
148 $url = CRM_Utils_System::url('civicrm/contact/' . $fragment, $urlParams);
149 $session = CRM_Core_Session::singleton();
150 $session->replaceUserContext($url);
151
152 $form->_task = CRM_Utils_Array::value('task', self::$_searchFormValues);
153 $crmContactTaskTasks = CRM_Contact_Task::taskTitles();
154 $form->assign('taskName', CRM_Utils_Array::value($form->_task, $crmContactTaskTasks));
155
156 if ($useTable) {
90e5222f 157 $tempTable = CRM_Utils_SQL_TempTable::build()->setCategory('tskact')->setDurable()->setId($qfKey)->setUtf8();
158 $form->_componentTable = $tempTable->getName();
159 $tempTable->drop();
160 $tempTable->createWithColumns('contact_id int primary key');
6a488035
TO
161 }
162
163 // all contacts or action = save a search
164 if ((CRM_Utils_Array::value('radio_ts', self::$_searchFormValues) == 'ts_all') ||
165 ($form->_task == CRM_Contact_Task::SAVE_SEARCH)
166 ) {
66ceb5d9 167 // since we don't store all contacts in prevnextcache, when user selects "all" use query to retrieve contacts
8e1a7c71 168 // rather than prevnext cache table for most of the task actions except export where we rebuild query to fetch
169 // final result set
170 if ($useTable) {
b7994703 171 $allCids = Civi::service('prevnext')->getSelection($cacheKey, "getall");
8e1a7c71 172 }
173 else {
fe9b66c1 174 $allCids[$cacheKey] = self::getContactIds($form);
8e1a7c71 175 }
6a488035 176
be2fb01f 177 $form->_contactIds = [];
6a488035
TO
178 if ($useTable) {
179 $count = 0;
be2fb01f 180 $insertString = [];
6a488035
TO
181 foreach ($allCids[$cacheKey] as $cid => $ignore) {
182 $count++;
183 $insertString[] = " ( {$cid} ) ";
184 if ($count % 200 == 0) {
185 $string = implode(',', $insertString);
186 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
187 CRM_Core_DAO::executeQuery($sql);
be2fb01f 188 $insertString = [];
6a488035
TO
189 }
190 }
191 if (!empty($insertString)) {
192 $string = implode(',', $insertString);
193 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
194 CRM_Core_DAO::executeQuery($sql);
195 }
196 }
061ff68c 197 elseif (empty($form->_contactIds)) {
6a488035
TO
198 // filter duplicates here
199 // CRM-7058
200 // might be better to do this in the query, but that logic is a bit complex
201 // and it decides when to use distinct based on input criteria, which needs
202 // to be fixed and optimized.
203
204 foreach ($allCids[$cacheKey] as $cid => $ignore) {
205 $form->_contactIds[] = $cid;
206 }
207 }
208 }
209 elseif (CRM_Utils_Array::value('radio_ts', self::$_searchFormValues) == 'ts_sel') {
210 // selected contacts only
211 // need to perform action on only selected contacts
be2fb01f 212 $insertString = [];
6a488035 213
b44e3f84 214 // refire sql in case of custom search
6a488035
TO
215 if ($form->_action == CRM_Core_Action::COPY) {
216 // selected contacts only
217 // need to perform action on only selected contacts
218 foreach (self::$_searchFormValues as $name => $value) {
219 if (substr($name, 0, CRM_Core_Form::CB_PREFIX_LEN) == CRM_Core_Form::CB_PREFIX) {
220 $contactID = substr($name, CRM_Core_Form::CB_PREFIX_LEN);
221 if ($useTable) {
222 $insertString[] = " ( {$contactID} ) ";
223 }
224 else {
225 $form->_contactIds[] = substr($name, CRM_Core_Form::CB_PREFIX_LEN);
226 }
227 }
228 }
229 }
230 else {
5d272ea2 231 // fetching selected contact ids of passed cache key
b7994703 232 $selectedCids = Civi::service('prevnext')->getSelection($cacheKey);
5d272ea2 233 foreach ($selectedCids[$cacheKey] as $selectedCid => $ignore) {
234 if ($useTable) {
235 $insertString[] = " ( {$selectedCid} ) ";
236 }
237 else {
238 $form->_contactIds[] = $selectedCid;
239 }
6a488035
TO
240 }
241 }
8ef12e64 242
6a488035
TO
243 if (!empty($insertString)) {
244 $string = implode(',', $insertString);
245 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
246 CRM_Core_DAO::executeQuery($sql);
247 }
248 }
249
250 //contact type for pick up profiles as per selected contact types with subtypes
251 //CRM-5521
252 if ($selectedTypes = CRM_Utils_Array::value('contact_type', self::$_searchFormValues)) {
253 if (!is_array($selectedTypes)) {
254 $selectedTypes = explode(' ', $selectedTypes);
255 }
256 foreach ($selectedTypes as $ct => $dontcare) {
257 if (strpos($ct, CRM_Core_DAO::VALUE_SEPARATOR) === FALSE) {
258 $form->_contactTypes[] = $ct;
259 }
260 else {
261 $separator = strpos($ct, CRM_Core_DAO::VALUE_SEPARATOR);
262 $form->_contactTypes[] = substr($ct, $separator + 1);
263 }
264 }
265 }
266
6a488035 267 if (CRM_Utils_Array::value('radio_ts', self::$_searchFormValues) == 'ts_sel'
5d272ea2 268 && ($form->_action != CRM_Core_Action::COPY)
269 ) {
6a488035 270 $sel = CRM_Utils_Array::value('radio_ts', self::$_searchFormValues);
5d272ea2 271 $form->assign('searchtype', $sel);
0dbe04b1 272 $result = self::getSelectedContactNames();
6a488035
TO
273 $form->assign("value", $result);
274 }
8ef12e64 275
6a488035
TO
276 if (!empty($form->_contactIds)) {
277 $form->_componentClause = ' contact_a.id IN ( ' . implode(',', $form->_contactIds) . ' ) ';
278 $form->assign('totalSelectedContacts', count($form->_contactIds));
279
280 $form->_componentIds = $form->_contactIds;
281 }
282 }
283
284 /**
fe9b66c1
MW
285 * Get the contact ids for:
286 * - "Select Records: All xx records"
287 * - custom search (FIXME: does this still apply to custom search?).
288 * When we call this function we are not using the prev/next cache
d8689418 289 *
fe9b66c1
MW
290 * @param $form CRM_Core_Form
291 *
292 * @return array $contactIds
6a488035 293 */
fe9b66c1 294 public static function getContactIds($form) {
6a488035
TO
295 // need to perform action on all contacts
296 // fire the query again and get the contact id's + display name
297 $sortID = NULL;
fe9b66c1
MW
298 if ($form->get(CRM_Utils_Sort::SORT_ID)) {
299 $sortID = CRM_Utils_Sort::sortIDValue($form->get(CRM_Utils_Sort::SORT_ID),
300 $form->get(CRM_Utils_Sort::SORT_DIRECTION)
6a488035
TO
301 );
302 }
303
fe9b66c1 304 $selectorName = $form->controller->selectorName();
6a488035 305
fe9b66c1
MW
306 $fv = $form->get('formValues');
307 $customClass = $form->get('customSearchClass');
6a488035
TO
308 $returnProperties = CRM_Core_BAO_Mapping::returnProperties(self::$_searchFormValues);
309
5d272ea2 310 $selector = new $selectorName($customClass, $fv, NULL, $returnProperties);
6a488035 311
fe9b66c1 312 $params = $form->get('queryParams');
6a488035
TO
313
314 // fix for CRM-5165
fe9b66c1 315 $sortByCharacter = $form->get('sortByCharacter');
a7cddb8c 316 if ($sortByCharacter && $sortByCharacter != 1) {
be2fb01f 317 $params[] = ['sortByCharacter', '=', $sortByCharacter, 0, 0];
6a488035 318 }
fe9b66c1 319 $queryOperator = $form->get('queryOperator');
6a488035
TO
320 if (!$queryOperator) {
321 $queryOperator = 'AND';
322 }
ae066a2b 323 $dao = $selector->contactIDQuery($params, $sortID,
5d272ea2 324 CRM_Utils_Array::value('display_relationship_type', $fv),
6a488035
TO
325 $queryOperator
326 );
327
be2fb01f 328 $contactIds = [];
5d272ea2 329 while ($dao->fetch()) {
6a488035
TO
330 $contactIds[$dao->contact_id] = $dao->contact_id;
331 }
332
333 return $contactIds;
334 }
335
6a488035 336 /**
ee0ce2ef 337 * Set default values for the form. Relationship that in edit/view action.
6a488035 338 *
ee0ce2ef 339 * The default values are retrieved from the database.
6a488035 340 *
e2046b33 341 * @return array
6a488035 342 */
00be9182 343 public function setDefaultValues() {
be2fb01f 344 $defaults = [];
6a488035
TO
345 return $defaults;
346 }
347
348 /**
57507ae6 349 * Add the rules for form.
6a488035 350 */
00be9182 351 public function addRules() {
5d272ea2 352 }
6a488035
TO
353
354 /**
5c9ff055 355 * Build the form object.
6a488035
TO
356 */
357 public function buildQuickForm() {
358 $this->addDefaultButtons(ts('Confirm Action'));
359 }
360
361 /**
ee0ce2ef 362 * Process the form after the input has been submitted and validated.
6a488035 363 */
5d272ea2 364 public function postProcess() {
365 }
366
6a488035 367 /**
57507ae6
EM
368 * Simple shell that derived classes can call to add form buttons.
369 *
370 * Allows customized title for the main Submit
6a488035 371 *
77c5b619
TO
372 * @param string $title
373 * Title of the main button.
374 * @param string $nextType
375 * Button type for the form after processing.
77b97be7
EM
376 * @param string $backType
377 * @param bool $submitOnce
6a488035 378 */
00be9182 379 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
be2fb01f 380 $this->addButtons([
69078420
SL
381 [
382 'type' => $nextType,
383 'name' => $title,
384 'isDefault' => TRUE,
385 ],
386 [
387 'type' => $backType,
388 'name' => ts('Cancel'),
389 'icon' => 'fa-times',
390 ],
391 ]);
6a488035 392 }
57884c26
DJ
393
394 /**
100fef9d 395 * Replace ids of household members in $this->_contactIds with the id of their household.
57884c26 396 *
5c9ff055 397 * CRM-8338
57884c26
DJ
398 */
399 public function mergeContactIdsByHousehold() {
400 if (empty($this->_contactIds)) {
401 return;
402 }
403
404 $contactRelationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
405 NULL,
406 NULL,
407 NULL,
408 NULL,
409 TRUE,
410 'name',
411 FALSE
412 );
413
414 // Get Head of Household & Household Member relationships
415 $relationKeyMOH = CRM_Utils_Array::key('Household Member of', $contactRelationshipTypes);
416 $relationKeyHOH = CRM_Utils_Array::key('Head of Household for', $contactRelationshipTypes);
be2fb01f 417 $householdRelationshipTypes = [
57884c26
DJ
418 $relationKeyMOH => $contactRelationshipTypes[$relationKeyMOH],
419 $relationKeyHOH => $contactRelationshipTypes[$relationKeyHOH],
be2fb01f 420 ];
57884c26
DJ
421
422 $relID = implode(',', $this->_contactIds);
423
424 foreach ($householdRelationshipTypes as $rel => $dnt) {
425 list($id, $direction) = explode('_', $rel, 2);
426 // identify the relationship direction
427 $contactA = 'contact_id_a';
428 $contactB = 'contact_id_b';
429 if ($direction == 'b_a') {
430 $contactA = 'contact_id_b';
431 $contactB = 'contact_id_a';
432 }
433
434 // Find related households.
353ffa53 435 $relationSelect = "SELECT contact_household.id as household_id, {$contactA} as refContact ";
57884c26
DJ
436 $relationFrom = " FROM civicrm_contact contact_household
437 INNER JOIN civicrm_relationship crel ON crel.{$contactB} = contact_household.id AND crel.relationship_type_id = {$id} ";
438
439 // Check for active relationship status only.
353ffa53
TO
440 $today = date('Ymd');
441 $relationActive = " AND (crel.is_active = 1 AND ( crel.end_date is NULL OR crel.end_date >= {$today} ) )";
442 $relationWhere = " WHERE contact_household.is_deleted = 0 AND crel.{$contactA} IN ( {$relID} ) {$relationActive}";
0ee5581e 443 $relationGroupBy = " GROUP BY crel.{$contactA}, contact_household.id";
57884c26
DJ
444 $relationQueryString = "$relationSelect $relationFrom $relationWhere $relationGroupBy";
445
446 $householdsDAO = CRM_Core_DAO::executeQuery($relationQueryString);
447 while ($householdsDAO->fetch()) {
448 // Remove contact's id from $this->_contactIds and replace with their household's id.
449 foreach (array_keys($this->_contactIds, $householdsDAO->refContact) as $idKey) {
450 unset($this->_contactIds[$idKey]);
451 }
452 if (!in_array($householdsDAO->household_id, $this->_contactIds)) {
453 $this->_contactIds[] = $householdsDAO->household_id;
454 }
455 }
57884c26 456 }
31fd348d
AS
457
458 // If contact list has changed, households will probably be at the end of
459 // the list. Sort it again by sort_name.
460 if (implode(',', $this->_contactIds) != $relID) {
be2fb01f
CW
461 $result = civicrm_api3('Contact', 'get', [
462 'return' => ['id'],
463 'id' => ['IN' => $this->_contactIds],
464 'options' => [
31fd348d 465 'limit' => 0,
351b2dd9 466 'sort' => "sort_name",
be2fb01f
CW
467 ],
468 ]);
31fd348d
AS
469 $this->_contactIds = array_keys($result['values']);
470 }
57884c26 471 }
96025800 472
0dbe04b1
TO
473 /**
474 * @return array
475 * List of contact names.
476 * NOTE: These are raw values from the DB. In current data-model, that means
477 * they are pre-encoded HTML.
478 */
479 private static function getSelectedContactNames() {
480 $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String');
481 $cacheKey = "civicrm search {$qfKey}";
482
be2fb01f 483 $cids = [];
0dbe04b1
TO
484 // Gymanstic time!
485 foreach (Civi::service('prevnext')->getSelection($cacheKey) as $cacheKey => $values) {
486 $cids = array_unique(array_merge($cids, array_keys($values)));
487 }
488
489 $result = CRM_Utils_SQL_Select::from('civicrm_contact')
490 ->where('id IN (#cids)', ['cids' => $cids])
491 ->execute()
492 ->fetchMap('id', 'sort_name');
493 return $result;
494 }
495
36c74cd6
TO
496 /**
497 * Given this task's list of targets, produce a hidden group.
498 *
499 * @return array
500 * Array(0 => int $groupID, 1 => int|NULL $ssID).
501 * @throws Exception
502 */
503 public function createHiddenGroup() {
504 // Did the user select "All" matches or cherry-pick a few records?
505 $searchParams = $this->controller->exportValues();
506 if ($searchParams['radio_ts'] == 'ts_sel') {
507 // Create a static group.
69078420
SL
508 // groups require a unique name
509 $randID = md5(time() . rand(1, 1000));
36c74cd6
TO
510 $grpTitle = "Hidden Group {$randID}";
511 $grpID = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Group', $grpTitle, 'id', 'title');
512
513 if (!$grpID) {
be2fb01f 514 $groupParams = [
36c74cd6
TO
515 'title' => $grpTitle,
516 'is_active' => 1,
517 'is_hidden' => 1,
be2fb01f
CW
518 'group_type' => ['2' => 1],
519 ];
36c74cd6
TO
520
521 $group = CRM_Contact_BAO_Group::create($groupParams);
522 $grpID = $group->id;
523
524 CRM_Contact_BAO_GroupContact::addContactsToGroup($this->_contactIds, $group->id);
525
526 $newGroupTitle = "Hidden Group {$grpID}";
be2fb01f 527 $groupParams = [
36c74cd6
TO
528 'id' => $grpID,
529 'name' => CRM_Utils_String::titleToVar($newGroupTitle),
530 'title' => $newGroupTitle,
be2fb01f
CW
531 'group_type' => ['2' => 1],
532 ];
5ab8fe4a 533 CRM_Contact_BAO_Group::create($groupParams);
36c74cd6
TO
534 }
535
536 // note at this point its a static group
be2fb01f 537 return [$grpID, NULL];
36c74cd6
TO
538 }
539 else {
540 // Create a smart group.
36c74cd6 541 $ssId = $this->get('ssID');
be2fb01f
CW
542 $hiddenSmartParams = [
543 'group_type' => ['2' => 1],
d1249113 544 // queryParams have been preprocessed esp WRT any entity reference fields - see +
545 // https://github.com/civicrm/civicrm-core/pull/13250
546 'form_values' => $this->get('queryParams'),
36c74cd6
TO
547 'saved_search_id' => $ssId,
548 'search_custom_id' => $this->get('customSearchID'),
549 'search_context' => $this->get('context'),
be2fb01f 550 ];
36c74cd6
TO
551
552 list($smartGroupId, $savedSearchId) = CRM_Contact_BAO_Group::createHiddenSmartGroup($hiddenSmartParams);
be2fb01f 553 return [$smartGroupId, $savedSearchId];
36c74cd6
TO
554 }
555
556 }
557
6a488035 558}