3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * This class generates form components for search-result tasks.
21 class CRM_Contact_Form_Task
extends CRM_Core_Form_Task
{
24 * The task being performed
31 * The array that holds all the contact ids
38 * The array that holds all the contact types
42 public $_contactTypes;
45 * The additional clause that we restrict the search with
49 protected $_componentClause = NULL;
52 * The name of the temp table where we store the contact IDs
56 protected $_componentTable = NULL;
59 * The array that holds all the component ids
63 protected $_componentIds;
66 * This includes the submitted values of the search form
69 static protected $_searchFormValues;
72 * Build all the data structures needed to build the form.
74 public function preProcess() {
75 self
::preProcessCommon($this);
79 * Common pre-processing function.
81 * @param \CRM_Core_Form_Task $form
83 * @throws \CRM_Core_Exception
85 public static function preProcessCommon(&$form) {
86 $form->_contactIds
= [];
87 $form->_contactTypes
= [];
89 $isStandAlone = in_array('task', $form->urlPath
) ||
in_array('standalone', $form->urlPath
);
91 [$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.'));
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'));
99 $form->setTitle($title);
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 $fragment .= '/advanced';
108 elseif ($form->_action
== CRM_Core_Action
::PROFILE
) {
109 $fragment .= '/builder';
111 elseif ($form->_action
== CRM_Core_Action
::COPY
) {
112 $fragment .= '/custom';
114 if (!$isStandAlone) {
115 self
::$_searchFormValues = $form->getSearchFormValues();
118 //set the user context for redirection of task actions
119 $qfKey = CRM_Utils_Request
::retrieve('qfKey', 'String', $form);
120 $urlParams = 'force=1';
121 if (CRM_Utils_Rule
::qfKey($qfKey)) {
122 $urlParams .= "&qfKey=$qfKey";
125 $url = CRM_Utils_System
::url('civicrm/contact/' . $fragment, $urlParams);
126 $session = CRM_Core_Session
::singleton();
127 $session->replaceUserContext($url);
129 $cacheKey = "civicrm search {$qfKey}";
131 $form->_task
= self
::$_searchFormValues['task'] ??
NULL;
133 // all contacts or action = save a search
134 if ((CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_all') ||
135 ($form->_task
== CRM_Contact_Task
::SAVE_SEARCH
)
137 // since we don't store all contacts in prevnextcache, when user selects "all" use query to retrieve contacts
138 // rather than prevnext cache table for most of the task actions except export where we rebuild query to fetch
140 $allCids[$cacheKey] = self
::getContactIds($form);
142 $form->_contactIds
= [];
143 if (empty($form->_contactIds
)) {
144 // filter duplicates here
146 // might be better to do this in the query, but that logic is a bit complex
147 // and it decides when to use distinct based on input criteria, which needs
148 // to be fixed and optimized.
150 foreach ($allCids[$cacheKey] as $cid => $ignore) {
151 $form->_contactIds
[] = $cid;
155 elseif (CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_sel') {
156 // selected contacts only
157 // need to perform action on only selected contacts
160 // refire sql in case of custom search
161 if ($form->_action
== CRM_Core_Action
::COPY
) {
162 // selected contacts only
163 // need to perform action on only selected contacts
164 foreach (self
::$_searchFormValues as $name => $value) {
165 if (substr($name, 0, CRM_Core_Form
::CB_PREFIX_LEN
) == CRM_Core_Form
::CB_PREFIX
) {
166 $form->_contactIds
[] = substr($name, CRM_Core_Form
::CB_PREFIX_LEN
);
171 // fetching selected contact ids of passed cache key
172 $selectedCids = Civi
::service('prevnext')->getSelection($cacheKey);
173 foreach ($selectedCids[$cacheKey] as $selectedCid => $ignore) {
174 $form->_contactIds
[] = $selectedCid;
178 if (!empty($insertString)) {
179 $string = implode(',', $insertString);
180 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
181 CRM_Core_DAO
::executeQuery($sql);
185 //contact type for pick up profiles as per selected contact types with subtypes
187 if ($selectedTypes = CRM_Utils_Array
::value('contact_type', self
::$_searchFormValues)) {
188 if (!is_array($selectedTypes)) {
189 $selectedTypes = explode(' ', $selectedTypes);
191 foreach ($selectedTypes as $ct => $dontcare) {
192 if (strpos($ct, CRM_Core_DAO
::VALUE_SEPARATOR
) === FALSE) {
193 $form->_contactTypes
[] = $ct;
196 $separator = strpos($ct, CRM_Core_DAO
::VALUE_SEPARATOR
);
197 $form->_contactTypes
[] = substr($ct, $separator +
1);
202 if (CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_sel'
203 && ($form->_action
!= CRM_Core_Action
::COPY
)
205 $sel = self
::$_searchFormValues['radio_ts'] ??
NULL;
206 $form->assign('searchtype', $sel);
207 $result = self
::getSelectedContactNames();
208 $form->assign("value", $result);
211 if (!empty($form->_contactIds
)) {
212 $form->_componentClause
= ' contact_a.id IN ( ' . implode(',', $form->_contactIds
) . ' ) ';
213 $form->assign('totalSelectedContacts', count($form->_contactIds
));
215 $form->_componentIds
= $form->_contactIds
;
220 * Get the contact ids for:
221 * - "Select Records: All xx records"
222 * - custom search (FIXME: does this still apply to custom search?).
223 * When we call this function we are not using the prev/next cache
225 * @param $form CRM_Core_Form
227 * @return array $contactIds
229 public static function getContactIds($form) {
230 // need to perform action on all contacts
231 // fire the query again and get the contact id's + display name
233 if ($form->get(CRM_Utils_Sort
::SORT_ID
)) {
234 $sortID = CRM_Utils_Sort
::sortIDValue($form->get(CRM_Utils_Sort
::SORT_ID
),
235 $form->get(CRM_Utils_Sort
::SORT_DIRECTION
)
239 $selectorName = $form->controller
->selectorName();
241 $fv = $form->get('formValues');
242 $customClass = $form->get('customSearchClass');
243 $returnProperties = CRM_Core_BAO_Mapping
::returnProperties(self
::$_searchFormValues);
245 $selector = new $selectorName($customClass, $fv, NULL, $returnProperties);
247 $params = $form->get('queryParams');
250 $sortByCharacter = $form->get('sortByCharacter');
251 if ($sortByCharacter && $sortByCharacter != 1) {
252 $params[] = ['sortByCharacter', '=', $sortByCharacter, 0, 0];
254 $queryOperator = $form->get('queryOperator');
255 if (!$queryOperator) {
256 $queryOperator = 'AND';
258 $dao = $selector->contactIDQuery($params, $sortID,
259 CRM_Utils_Array
::value('display_relationship_type', $fv),
264 while ($dao->fetch()) {
265 $contactIds[$dao->contact_id
] = $dao->contact_id
;
272 * Set default values for the form. Relationship that in edit/view action.
274 * The default values are retrieved from the database.
278 public function setDefaultValues() {
284 * Add the rules for form.
286 public function addRules() {
290 * Build the form object.
292 public function buildQuickForm() {
293 $this->addDefaultButtons(ts('Confirm Action'));
297 * Process the form after the input has been submitted and validated.
299 public function postProcess() {
303 * Simple shell that derived classes can call to add form buttons.
305 * Allows customized title for the main Submit
307 * @param string $title
308 * Title of the main button.
309 * @param string $nextType
310 * Button type for the form after processing.
311 * @param string $backType
312 * @param bool $submitOnce
314 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
323 'name' => ts('Cancel'),
324 'icon' => 'fa-times',
330 * Replace ids of household members in $this->_contactIds with the id of their household.
332 * @see https://issues.civicrm.org/jira/browse/CRM-8338
334 public function mergeContactIdsByHousehold() {
335 if (empty($this->_contactIds
)) {
339 $contactRelationshipTypes = CRM_Contact_BAO_Relationship
::getContactRelationshipType(
349 // Get Head of Household & Household Member relationships
350 $relationKeyMOH = CRM_Utils_Array
::key('Household Member of', $contactRelationshipTypes);
351 $relationKeyHOH = CRM_Utils_Array
::key('Head of Household for', $contactRelationshipTypes);
352 $householdRelationshipTypes = [
353 $relationKeyMOH => $contactRelationshipTypes[$relationKeyMOH],
354 $relationKeyHOH => $contactRelationshipTypes[$relationKeyHOH],
357 $relID = implode(',', $this->_contactIds
);
359 foreach ($householdRelationshipTypes as $rel => $dnt) {
360 list($id, $direction) = explode('_', $rel, 2);
361 // identify the relationship direction
362 $contactA = 'contact_id_a';
363 $contactB = 'contact_id_b';
364 if ($direction == 'b_a') {
365 $contactA = 'contact_id_b';
366 $contactB = 'contact_id_a';
369 // Find related households.
370 $relationSelect = "SELECT contact_household.id as household_id, {$contactA} as refContact ";
371 $relationFrom = " FROM civicrm_contact contact_household
372 INNER JOIN civicrm_relationship crel ON crel.{$contactB} = contact_household.id AND crel.relationship_type_id = {$id} ";
374 // Check for active relationship status only.
375 $today = date('Ymd');
376 $relationActive = " AND (crel.is_active = 1 AND ( crel.end_date is NULL OR crel.end_date >= {$today} ) )";
377 $relationWhere = " WHERE contact_household.is_deleted = 0 AND crel.{$contactA} IN ( {$relID} ) {$relationActive}";
378 $relationGroupBy = " GROUP BY crel.{$contactA}, contact_household.id";
379 $relationQueryString = "$relationSelect $relationFrom $relationWhere $relationGroupBy";
381 $householdsDAO = CRM_Core_DAO
::executeQuery($relationQueryString);
382 while ($householdsDAO->fetch()) {
383 // Remove contact's id from $this->_contactIds and replace with their household's id.
384 foreach (array_keys($this->_contactIds
, $householdsDAO->refContact
) as $idKey) {
385 unset($this->_contactIds
[$idKey]);
387 if (!in_array($householdsDAO->household_id
, $this->_contactIds
)) {
388 $this->_contactIds
[] = $householdsDAO->household_id
;
393 // If contact list has changed, households will probably be at the end of
394 // the list. Sort it again by sort_name.
395 if (implode(',', $this->_contactIds
) != $relID) {
396 $result = civicrm_api3('Contact', 'get', [
398 'id' => ['IN' => $this->_contactIds
],
401 'sort' => "sort_name",
404 $this->_contactIds
= array_keys($result['values']);
410 * List of contact names.
411 * NOTE: These are raw values from the DB. In current data-model, that means
412 * they are pre-encoded HTML.
414 private static function getSelectedContactNames() {
415 $qfKey = CRM_Utils_Request
::retrieve('qfKey', 'String');
416 $cacheKey = "civicrm search {$qfKey}";
420 foreach (Civi
::service('prevnext')->getSelection($cacheKey) as $cacheKey => $values) {
421 $cids = array_unique(array_merge($cids, array_keys($values)));
424 $result = CRM_Utils_SQL_Select
::from('civicrm_contact')
425 ->where('id IN (#cids)', ['cids' => $cids])
427 ->fetchMap('id', 'sort_name');
432 * Given this task's list of targets, produce a hidden group.
435 * Array(0 => int $groupID, 1 => int|NULL $ssID).
438 public function createHiddenGroup() {
439 // Did the user select "All" matches or cherry-pick a few records?
440 $searchParams = $this->controller
->exportValues();
441 if ($searchParams['radio_ts'] == 'ts_sel') {
442 // Create a static group.
443 // groups require a unique name
444 $randID = md5(time() . rand(1, 1000));
445 $grpTitle = "Hidden Group {$randID}";
446 $grpID = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Group', $grpTitle, 'id', 'title');
450 'title' => $grpTitle,
453 'group_type' => ['2' => 1],
456 $group = CRM_Contact_BAO_Group
::create($groupParams);
459 CRM_Contact_BAO_GroupContact
::addContactsToGroup($this->_contactIds
, $group->id
);
461 $newGroupTitle = "Hidden Group {$grpID}";
464 'name' => CRM_Utils_String
::titleToVar($newGroupTitle),
465 'title' => $newGroupTitle,
466 'group_type' => ['2' => 1],
468 CRM_Contact_BAO_Group
::create($groupParams);
471 // note at this point its a static group
472 return [$grpID, NULL];
475 // Create a smart group.
476 $ssId = $this->get('ssID');
477 $hiddenSmartParams = [
478 'group_type' => ['2' => 1],
479 // queryParams have been preprocessed esp WRT any entity reference fields - see +
480 // https://github.com/civicrm/civicrm-core/pull/13250
481 // Advanced search sets queryParams, for builder you need formValues.
482 // This is kinda fragile but .... see CRM_Mailing_Form_Task_AdhocMailingTest for test effort.
483 // Moral never touch anything ever again and the house of cards will stand tall, unless there is a breeze
484 'form_values' => $this->get('isSearchBuilder') ?
$this->get('formValues') : $this->get('queryParams'),
485 'saved_search_id' => $ssId,
486 'search_custom_id' => $this->get('customSearchID'),
487 'search_context' => $this->get('context'),
490 list($smartGroupId, $savedSearchId) = CRM_Contact_BAO_Group
::createHiddenSmartGroup($hiddenSmartParams);
491 return [$smartGroupId, $savedSearchId];