3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2019
35 * This class generates form components for search-result tasks.
37 class CRM_Contact_Form_Task
extends CRM_Core_Form_Task
{
40 * The task being performed
47 * The array that holds all the contact ids
54 * The array that holds all the contact types
58 public $_contactTypes;
61 * The additional clause that we restrict the search with
65 protected $_componentClause = NULL;
68 * The name of the temp table where we store the contact IDs
72 protected $_componentTable = NULL;
75 * The array that holds all the component ids
79 protected $_componentIds;
82 * This includes the submitted values of the search form
84 static protected $_searchFormValues;
87 * Build all the data structures needed to build the form.
89 public function preProcess() {
90 self
::preProcessCommon($this);
94 * Common pre-processing function.
96 * @param CRM_Core_Form $form
98 * @throws \CRM_Core_Exception
100 public static function preProcessCommon(&$form) {
101 $form->_contactIds
= array();
102 $form->_contactTypes
= array();
104 $useTable = (CRM_Utils_System
::getClassName($form->controller
->getStateMachine()) == 'CRM_Export_StateMachine_Standalone');
106 $isStandAlone = in_array('task', $form->urlPath
) ||
in_array('standalone', $form->urlPath
);
108 list($form->_task
, $title) = CRM_Contact_Task
::getTaskAndTitleByClass(get_class($form));
109 if (!array_key_exists($form->_task
, CRM_Contact_Task
::permissionedTaskTitles(CRM_Core_Permission
::getPermission()))) {
110 CRM_Core_Error
::statusBounce(ts('You do not have permission to access this page.'));
112 $form->_contactIds
= explode(',', CRM_Utils_Request
::retrieve('cids', 'CommaSeparatedIntegers', $form, TRUE));
113 if (empty($form->_contactIds
)) {
114 CRM_Core_Error
::statusBounce(ts('No Contacts Selected'));
116 $form->setTitle($title);
119 // get the submitted values of the search form
120 // we'll need to get fv from either search or adv search in the future
121 $fragment = 'search';
122 if ($form->_action
== CRM_Core_Action
::ADVANCED
) {
123 self
::$_searchFormValues = $form->controller
->exportValues('Advanced');
124 $fragment .= '/advanced';
126 elseif ($form->_action
== CRM_Core_Action
::PROFILE
) {
127 self
::$_searchFormValues = $form->controller
->exportValues('Builder');
128 $fragment .= '/builder';
130 elseif ($form->_action
== CRM_Core_Action
::COPY
) {
131 self
::$_searchFormValues = $form->controller
->exportValues('Custom');
132 $fragment .= '/custom';
134 elseif (!$isStandAlone) {
135 self
::$_searchFormValues = $form->controller
->exportValues('Basic');
138 //set the user context for redirection of task actions
139 $qfKey = CRM_Utils_Request
::retrieve('qfKey', 'String', $form);
140 $urlParams = 'force=1';
141 if (CRM_Utils_Rule
::qfKey($qfKey)) {
142 $urlParams .= "&qfKey=$qfKey";
145 $cacheKey = "civicrm search {$qfKey}";
147 $url = CRM_Utils_System
::url('civicrm/contact/' . $fragment, $urlParams);
148 $session = CRM_Core_Session
::singleton();
149 $session->replaceUserContext($url);
151 $form->_task
= CRM_Utils_Array
::value('task', self
::$_searchFormValues);
152 $crmContactTaskTasks = CRM_Contact_Task
::taskTitles();
153 $form->assign('taskName', CRM_Utils_Array
::value($form->_task
, $crmContactTaskTasks));
156 $form->_componentTable
= CRM_Utils_SQL_TempTable
::build()->setCategory('tskact')->setDurable()->setId($qfKey)->getName();
157 $sql = " DROP TABLE IF EXISTS {$form->_componentTable}";
158 CRM_Core_DAO
::executeQuery($sql);
160 $sql = "CREATE TABLE {$form->_componentTable} ( contact_id int primary key) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci";
161 CRM_Core_DAO
::executeQuery($sql);
164 // all contacts or action = save a search
165 if ((CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_all') ||
166 ($form->_task
== CRM_Contact_Task
::SAVE_SEARCH
)
168 // since we don't store all contacts in prevnextcache, when user selects "all" use query to retrieve contacts
169 // rather than prevnext cache table for most of the task actions except export where we rebuild query to fetch
172 $allCids = Civi
::service('prevnext')->getSelection($cacheKey, "getall");
175 $allCids[$cacheKey] = self
::getContactIds($form);
178 $form->_contactIds
= array();
181 $insertString = array();
182 foreach ($allCids[$cacheKey] as $cid => $ignore) {
184 $insertString[] = " ( {$cid} ) ";
185 if ($count %
200 == 0) {
186 $string = implode(',', $insertString);
187 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
188 CRM_Core_DAO
::executeQuery($sql);
189 $insertString = array();
192 if (!empty($insertString)) {
193 $string = implode(',', $insertString);
194 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
195 CRM_Core_DAO
::executeQuery($sql);
198 elseif (empty($form->_contactIds
)) {
199 // filter duplicates here
201 // might be better to do this in the query, but that logic is a bit complex
202 // and it decides when to use distinct based on input criteria, which needs
203 // to be fixed and optimized.
205 foreach ($allCids[$cacheKey] as $cid => $ignore) {
206 $form->_contactIds
[] = $cid;
210 elseif (CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_sel') {
211 // selected contacts only
212 // need to perform action on only selected contacts
213 $insertString = array();
215 // refire sql in case of custom search
216 if ($form->_action
== CRM_Core_Action
::COPY
) {
217 // selected contacts only
218 // need to perform action on only selected contacts
219 foreach (self
::$_searchFormValues as $name => $value) {
220 if (substr($name, 0, CRM_Core_Form
::CB_PREFIX_LEN
) == CRM_Core_Form
::CB_PREFIX
) {
221 $contactID = substr($name, CRM_Core_Form
::CB_PREFIX_LEN
);
223 $insertString[] = " ( {$contactID} ) ";
226 $form->_contactIds
[] = substr($name, CRM_Core_Form
::CB_PREFIX_LEN
);
232 // fetching selected contact ids of passed cache key
233 $selectedCids = Civi
::service('prevnext')->getSelection($cacheKey);
234 foreach ($selectedCids[$cacheKey] as $selectedCid => $ignore) {
236 $insertString[] = " ( {$selectedCid} ) ";
239 $form->_contactIds
[] = $selectedCid;
244 if (!empty($insertString)) {
245 $string = implode(',', $insertString);
246 $sql = "REPLACE INTO {$form->_componentTable} ( contact_id ) VALUES $string";
247 CRM_Core_DAO
::executeQuery($sql);
251 //contact type for pick up profiles as per selected contact types with subtypes
253 if ($selectedTypes = CRM_Utils_Array
::value('contact_type', self
::$_searchFormValues)) {
254 if (!is_array($selectedTypes)) {
255 $selectedTypes = explode(' ', $selectedTypes);
257 foreach ($selectedTypes as $ct => $dontcare) {
258 if (strpos($ct, CRM_Core_DAO
::VALUE_SEPARATOR
) === FALSE) {
259 $form->_contactTypes
[] = $ct;
262 $separator = strpos($ct, CRM_Core_DAO
::VALUE_SEPARATOR
);
263 $form->_contactTypes
[] = substr($ct, $separator +
1);
268 if (CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues) == 'ts_sel'
269 && ($form->_action
!= CRM_Core_Action
::COPY
)
271 $sel = CRM_Utils_Array
::value('radio_ts', self
::$_searchFormValues);
272 $form->assign('searchtype', $sel);
273 $result = self
::getSelectedContactNames();
274 $form->assign("value", $result);
277 if (!empty($form->_contactIds
)) {
278 $form->_componentClause
= ' contact_a.id IN ( ' . implode(',', $form->_contactIds
) . ' ) ';
279 $form->assign('totalSelectedContacts', count($form->_contactIds
));
281 $form->_componentIds
= $form->_contactIds
;
286 * Get the contact ids for:
287 * - "Select Records: All xx records"
288 * - custom search (FIXME: does this still apply to custom search?).
289 * When we call this function we are not using the prev/next cache
291 * @param $form CRM_Core_Form
293 * @return array $contactIds
295 public static function getContactIds($form) {
296 // need to perform action on all contacts
297 // fire the query again and get the contact id's + display name
299 if ($form->get(CRM_Utils_Sort
::SORT_ID
)) {
300 $sortID = CRM_Utils_Sort
::sortIDValue($form->get(CRM_Utils_Sort
::SORT_ID
),
301 $form->get(CRM_Utils_Sort
::SORT_DIRECTION
)
305 $selectorName = $form->controller
->selectorName();
307 $fv = $form->get('formValues');
308 $customClass = $form->get('customSearchClass');
309 $returnProperties = CRM_Core_BAO_Mapping
::returnProperties(self
::$_searchFormValues);
311 $selector = new $selectorName($customClass, $fv, NULL, $returnProperties);
313 $params = $form->get('queryParams');
316 $sortByCharacter = $form->get('sortByCharacter');
317 if ($sortByCharacter && $sortByCharacter != 1) {
318 $params[] = array('sortByCharacter', '=', $sortByCharacter, 0, 0);
320 $queryOperator = $form->get('queryOperator');
321 if (!$queryOperator) {
322 $queryOperator = 'AND';
324 $dao = $selector->contactIDQuery($params, $sortID,
325 CRM_Utils_Array
::value('display_relationship_type', $fv),
329 $contactIds = array();
330 while ($dao->fetch()) {
331 $contactIds[$dao->contact_id
] = $dao->contact_id
;
339 * Set default values for the form. Relationship that in edit/view action.
341 * The default values are retrieved from the database.
345 public function setDefaultValues() {
351 * Add the rules for form.
353 public function addRules() {
357 * Build the form object.
359 public function buildQuickForm() {
360 $this->addDefaultButtons(ts('Confirm Action'));
364 * Process the form after the input has been submitted and validated.
366 public function postProcess() {
370 * Simple shell that derived classes can call to add form buttons.
372 * Allows customized title for the main Submit
374 * @param string $title
375 * Title of the main button.
376 * @param string $nextType
377 * Button type for the form after processing.
378 * @param string $backType
379 * @param bool $submitOnce
381 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
382 $this->addButtons(array(
390 'name' => ts('Cancel'),
391 'icon' => 'fa-times',
398 * Replace ids of household members in $this->_contactIds with the id of their household.
402 public function mergeContactIdsByHousehold() {
403 if (empty($this->_contactIds
)) {
407 $contactRelationshipTypes = CRM_Contact_BAO_Relationship
::getContactRelationshipType(
417 // Get Head of Household & Household Member relationships
418 $relationKeyMOH = CRM_Utils_Array
::key('Household Member of', $contactRelationshipTypes);
419 $relationKeyHOH = CRM_Utils_Array
::key('Head of Household for', $contactRelationshipTypes);
420 $householdRelationshipTypes = array(
421 $relationKeyMOH => $contactRelationshipTypes[$relationKeyMOH],
422 $relationKeyHOH => $contactRelationshipTypes[$relationKeyHOH],
425 $relID = implode(',', $this->_contactIds
);
427 foreach ($householdRelationshipTypes as $rel => $dnt) {
428 list($id, $direction) = explode('_', $rel, 2);
429 // identify the relationship direction
430 $contactA = 'contact_id_a';
431 $contactB = 'contact_id_b';
432 if ($direction == 'b_a') {
433 $contactA = 'contact_id_b';
434 $contactB = 'contact_id_a';
437 // Find related households.
438 $relationSelect = "SELECT contact_household.id as household_id, {$contactA} as refContact ";
439 $relationFrom = " FROM civicrm_contact contact_household
440 INNER JOIN civicrm_relationship crel ON crel.{$contactB} = contact_household.id AND crel.relationship_type_id = {$id} ";
442 // Check for active relationship status only.
443 $today = date('Ymd');
444 $relationActive = " AND (crel.is_active = 1 AND ( crel.end_date is NULL OR crel.end_date >= {$today} ) )";
445 $relationWhere = " WHERE contact_household.is_deleted = 0 AND crel.{$contactA} IN ( {$relID} ) {$relationActive}";
446 $relationGroupBy = " GROUP BY crel.{$contactA}, contact_household.id";
447 $relationQueryString = "$relationSelect $relationFrom $relationWhere $relationGroupBy";
449 $householdsDAO = CRM_Core_DAO
::executeQuery($relationQueryString);
450 while ($householdsDAO->fetch()) {
451 // Remove contact's id from $this->_contactIds and replace with their household's id.
452 foreach (array_keys($this->_contactIds
, $householdsDAO->refContact
) as $idKey) {
453 unset($this->_contactIds
[$idKey]);
455 if (!in_array($householdsDAO->household_id
, $this->_contactIds
)) {
456 $this->_contactIds
[] = $householdsDAO->household_id
;
459 $householdsDAO->free();
462 // If contact list has changed, households will probably be at the end of
463 // the list. Sort it again by sort_name.
464 if (implode(',', $this->_contactIds
) != $relID) {
465 $result = civicrm_api3('Contact', 'get', array(
466 'return' => array('id'),
467 'id' => array('IN' => $this->_contactIds
),
470 'sort' => "sort_name",
473 $this->_contactIds
= array_keys($result['values']);
479 * List of contact names.
480 * NOTE: These are raw values from the DB. In current data-model, that means
481 * they are pre-encoded HTML.
483 private static function getSelectedContactNames() {
484 $qfKey = CRM_Utils_Request
::retrieve('qfKey', 'String');
485 $cacheKey = "civicrm search {$qfKey}";
489 foreach (Civi
::service('prevnext')->getSelection($cacheKey) as $cacheKey => $values) {
490 $cids = array_unique(array_merge($cids, array_keys($values)));
493 $result = CRM_Utils_SQL_Select
::from('civicrm_contact')
494 ->where('id IN (#cids)', ['cids' => $cids])
496 ->fetchMap('id', 'sort_name');
501 * Given this task's list of targets, produce a hidden group.
504 * Array(0 => int $groupID, 1 => int|NULL $ssID).
507 public function createHiddenGroup() {
508 // Did the user select "All" matches or cherry-pick a few records?
509 $searchParams = $this->controller
->exportValues();
510 if ($searchParams['radio_ts'] == 'ts_sel') {
511 // Create a static group.
512 $randID = md5(time() . rand(1, 1000)); // groups require a unique name
513 $grpTitle = "Hidden Group {$randID}";
514 $grpID = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Group', $grpTitle, 'id', 'title');
517 $groupParams = array(
518 'title' => $grpTitle,
521 'group_type' => array('2' => 1),
524 $group = CRM_Contact_BAO_Group
::create($groupParams);
527 CRM_Contact_BAO_GroupContact
::addContactsToGroup($this->_contactIds
, $group->id
);
529 $newGroupTitle = "Hidden Group {$grpID}";
530 $groupParams = array(
532 'name' => CRM_Utils_String
::titleToVar($newGroupTitle),
533 'title' => $newGroupTitle,
534 'group_type' => array('2' => 1),
536 CRM_Contact_BAO_Group
::create($groupParams);
539 // note at this point its a static group
540 return array($grpID, NULL);
543 // Create a smart group.
544 $ssId = $this->get('ssID');
545 $hiddenSmartParams = array(
546 'group_type' => array('2' => 1),
547 'form_values' => $this->get('formValues'),
548 'saved_search_id' => $ssId,
549 'search_custom_id' => $this->get('customSearchID'),
550 'search_context' => $this->get('context'),
553 list($smartGroupId, $savedSearchId) = CRM_Contact_BAO_Group
::createHiddenSmartGroup($hiddenSmartParams);
554 return array($smartGroupId, $savedSearchId);