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