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