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