| 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 | } |