dev/core#1098 Add unit test & code comments relating to the slow activity search
[civicrm-core.git] / CRM / Activity / Selector / Search.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 is used to retrieve and display a range of contacts that match the given criteria.
36 *
37 * Specifically for results of advanced search options.
38 */
39 class CRM_Activity_Selector_Search extends CRM_Core_Selector_Base implements CRM_Core_Selector_API {
40
41 /**
42 * This defines two actions- View and Edit.
43 *
44 * @var array
45 */
46 public static $_links = NULL;
47
48 /**
49 * We use desc to remind us what that column is, name is used in the tpl
50 *
51 * @var array
52 */
53 public static $_columnHeaders;
54
55 /**
56 * Properties of contact we're interested in displaying
57 * @var array
58 */
59
60 public static $_properties = [
61 'contact_id',
62 'contact_type',
63 'contact_sub_type',
64 'sort_name',
65 'display_name',
66 'activity_id',
67 'activity_date_time',
68 'activity_status_id',
69 'activity_status',
70 'activity_subject',
71 'source_record_id',
72 'activity_type_id',
73 'activity_type',
74 'activity_is_test',
75 'activity_campaign_id',
76 'activity_engagement_level',
77 ];
78
79 /**
80 * Are we restricting ourselves to a single contact.
81 *
82 * @var bool
83 */
84 protected $_single = FALSE;
85
86 /**
87 * Are we restricting ourselves to a single contact.
88 *
89 * @var bool
90 */
91 protected $_limit = NULL;
92
93 /**
94 * What context are we being invoked from.
95 *
96 * @var string
97 */
98 protected $_context = NULL;
99
100 /**
101 * What component context are we being invoked from.
102 *
103 * @var string
104 */
105 protected $_compContext = NULL;
106
107 /**
108 * QueryParams is the array returned by exportValues called on.
109 * the HTML_QuickForm_Controller for that page.
110 *
111 * @var array
112 */
113 public $_queryParams;
114
115 /**
116 * Represent the type of selector.
117 *
118 * @var int
119 */
120 protected $_action;
121
122 /**
123 * The additional clause that we restrict the search with.
124 *
125 * @var string
126 */
127 protected $_activityClause = NULL;
128
129 /**
130 * The query object.
131 *
132 * @var \CRM_Contact_BAO_Query
133 */
134 protected $_query;
135
136 /**
137 * Class constructor.
138 *
139 * @param array $queryParams
140 * Array of parameters for query.
141 * @param \const|int $action - action of search basic or advanced.
142 * @param string $activityClause
143 * If the caller wants to further restrict the search (used in activities).
144 * @param bool $single
145 * Are we dealing only with one contact?.
146 * @param int $limit
147 * How many activities do we want returned.
148 *
149 * @param string $context
150 * @param null $compContext
151 *
152 * @return \CRM_Activity_Selector_Search
153 */
154 public function __construct(
155 &$queryParams,
156 $action = CRM_Core_Action::NONE,
157 $activityClause = NULL,
158 $single = FALSE,
159 $limit = NULL,
160 $context = 'search',
161 $compContext = NULL
162 ) {
163 // submitted form values
164 $this->_queryParams = &$queryParams;
165
166 $this->_single = $single;
167 $this->_limit = $limit;
168 $this->_context = $context;
169 $this->_compContext = $compContext;
170
171 $this->_activityClause = $activityClause;
172
173 // CRM-12675
174 $components = CRM_Core_Component::getNames();
175 $componentClause = [];
176 foreach ($components as $componentID => $componentName) {
177 // CRM-19201: Add support for searching CiviCampaign and CiviCase
178 // activities. For CiviCase, "access all cases and activities" is
179 // required here rather than "access my cases and activities" to
180 // prevent those with only the later permission from seeing a list
181 // of all cases which might present a privacy issue.
182 // @todo this is the cause of the current devastatingly bad performance on
183 // activity search - it involves a bad join.
184 // The correct fix is to use the permission infrastrucutre - ie. add in the
185 // clause generated by CRM_Activity_BAO_Query::addSelectWhere
186 // but some testing needs to check that before making the change
187 // see https://github.com/civicrm/civicrm-core/blob/be2fb01f90f5f299dd07402a41fed7c7c7567f00/CRM/Utils/SQL.php#L48
188 // for how it's done in the api kernel.
189 if (!CRM_Core_Permission::access($componentName, TRUE, TRUE)) {
190 $componentClause[] = " (activity_type.component_id IS NULL OR activity_type.component_id <> {$componentID}) ";
191 }
192 }
193
194 if (!empty($componentClause)) {
195 $componentRestriction = implode(' AND ', $componentClause);
196 if (empty($this->_activityClause)) {
197 $this->_activityClause = $componentRestriction;
198 }
199 else {
200 $this->_activityClause .= ' AND ' . $componentRestriction;
201 }
202 }
203
204 // type of selector
205 $this->_action = $action;
206 $this->_query = new CRM_Contact_BAO_Query($this->_queryParams,
207 CRM_Activity_BAO_Query::selectorReturnProperties(),
208 NULL, FALSE, FALSE,
209 CRM_Contact_BAO_Query::MODE_ACTIVITY
210 );
211 $this->_query->_distinctComponentClause = '( civicrm_activity.id )';
212 $this->_query->_groupByComponentClause = " GROUP BY civicrm_activity.id ";
213 }
214
215 /**
216 * Getter for array of the parameters required for creating pager.
217 *
218 * @param $action
219 * @param array $params
220 */
221 public function getPagerParams($action, &$params) {
222 $params['status'] = ts('Activities %%StatusMessage%%');
223 $params['csvString'] = NULL;
224 $params['rowCount'] = CRM_Utils_Pager::ROWCOUNT;
225 $params['buttonTop'] = 'PagerTopButton';
226 $params['buttonBottom'] = 'PagerBottomButton';
227 }
228
229 /**
230 * Returns total number of rows for the query.
231 *
232 * @param string $action
233 *
234 * @return int
235 * Total number of rows
236 */
237 public function getTotalCount($action) {
238 return $this->_query->searchQuery(0, 0, NULL,
239 TRUE, FALSE,
240 FALSE, FALSE,
241 FALSE,
242 $this->_activityClause
243 );
244 }
245
246 /**
247 * Returns all the rows in the given offset and rowCount.
248 *
249 * @param string $action
250 * The action being performed.
251 * @param int $offset
252 * The row number to start from.
253 * @param int $rowCount
254 * The number of rows to return.
255 * @param string $sort
256 * The sql string that describes the sort order.
257 * @param string $output
258 * What should the result set include (web/email/csv).
259 *
260 * @return array
261 * rows in the given offset and rowCount
262 */
263 public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) {
264 $result = $this->_query->searchQuery(
265 $offset, $rowCount, $sort,
266 FALSE, FALSE,
267 FALSE, FALSE,
268 FALSE,
269 $this->_activityClause
270 );
271 $rows = [];
272 $mailingIDs = CRM_Mailing_BAO_Mailing::mailingACLIDs();
273 $accessCiviMail = CRM_Core_Permission::check('access CiviMail');
274
275 // Get all campaigns.
276 $allCampaigns = CRM_Campaign_BAO_Campaign::getCampaigns(NULL, NULL, FALSE, FALSE, FALSE, TRUE);
277
278 $engagementLevels = CRM_Campaign_PseudoConstant::engagementLevel();
279 $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
280 $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts);
281 $assigneeID = CRM_Utils_Array::key('Activity Assignees', $activityContacts);
282 $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
283 $bulkActivityTypeID = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Bulk Email');
284
285 while ($result->fetch()) {
286 $row = [];
287
288 // Ignore rows where we dont have an activity id.
289 if (empty($result->activity_id)) {
290 continue;
291 }
292 $this->_query->convertToPseudoNames($result);
293
294 // the columns we are interested in
295 foreach (self::$_properties as $property) {
296 if (isset($result->$property)) {
297 $row[$property] = $result->$property;
298 }
299 }
300
301 $contactId = CRM_Utils_Array::value('contact_id', $row);
302 if (!$contactId) {
303 $contactId = CRM_Utils_Array::value('source_contact_id', $row);
304 }
305
306 $row['target_contact_name'] = CRM_Activity_BAO_ActivityContact::getNames($row['activity_id'], $targetID);
307 $row['assignee_contact_name'] = CRM_Activity_BAO_ActivityContact::getNames($row['activity_id'], $assigneeID);
308 list($row['source_contact_name'], $row['source_contact_id']) = CRM_Activity_BAO_ActivityContact::getNames($row['activity_id'], $sourceID, TRUE);
309 $row['source_contact_name'] = implode(',', array_values($row['source_contact_name']));
310 $row['source_contact_id'] = implode(',', $row['source_contact_id']);
311
312 if ($this->_context == 'search') {
313 $row['checkbox'] = CRM_Core_Form::CB_PREFIX . $result->activity_id;
314 }
315 $row['contact_type'] = CRM_Contact_BAO_Contact_Utils::getImage($result->contact_sub_type ? $result->contact_sub_type : $result->contact_type, FALSE, $result->contact_id
316 );
317 $accessMailingReport = FALSE;
318 $activityTypeId = $row['activity_type_id'];
319 if ($row['activity_is_test']) {
320 $row['activity_type'] = CRM_Core_TestEntity::appendTestText($row['activity_type']);
321 }
322 $row['mailingId'] = '';
323 if (
324 $accessCiviMail &&
325 ($mailingIDs === TRUE || in_array($result->source_record_id, $mailingIDs)) &&
326 ($bulkActivityTypeID == $activityTypeId)
327 ) {
328 $row['mailingId'] = CRM_Utils_System::url('civicrm/mailing/report',
329 "mid={$result->source_record_id}&reset=1&cid={$contactId}&context=activitySelector"
330 );
331 $row['recipients'] = ts('(recipients)');
332 $row['target_contact_name'] = '';
333 $row['assignee_contact_name'] = '';
334 $accessMailingReport = TRUE;
335 }
336 $activityActions = new CRM_Activity_Selector_Activity($result->contact_id, NULL);
337 $actionLinks = $activityActions->actionLinks($activityTypeId,
338 CRM_Utils_Array::value('source_record_id', $row),
339 $accessMailingReport,
340 CRM_Utils_Array::value('activity_id', $row),
341 $this->_key,
342 $this->_compContext
343 );
344 $row['action'] = CRM_Core_Action::formLink($actionLinks, NULL,
345 [
346 'id' => $result->activity_id,
347 'cid' => $contactId,
348 'cxt' => $this->_context,
349 ],
350 ts('more'),
351 FALSE,
352 'activity.selector.row',
353 'Activity',
354 $result->activity_id
355 );
356
357 // Carry campaign to selector.
358 $row['campaign'] = CRM_Utils_Array::value($result->activity_campaign_id, $allCampaigns);
359 $row['campaign_id'] = $result->activity_campaign_id;
360
361 if ($engagementLevel = CRM_Utils_Array::value('activity_engagement_level', $row)) {
362 $row['activity_engagement_level'] = CRM_Utils_Array::value($engagementLevel,
363 $engagementLevels, $engagementLevel
364 );
365 }
366
367 // Check if recurring activity.
368 $repeat = CRM_Core_BAO_RecurringEntity::getPositionAndCount($row['activity_id'], 'civicrm_activity');
369 $row['repeat'] = '';
370 if ($repeat) {
371 $row['repeat'] = ts('Repeating (%1 of %2)', [1 => $repeat[0], 2 => $repeat[1]]);
372 }
373 $rows[] = $row;
374 }
375
376 return $rows;
377 }
378
379 /**
380 * @return array
381 * which contains an array of strings
382 */
383 public function getQILL() {
384 return $this->_query->qill();
385 }
386
387 /**
388 * Returns the column headers as an array of tuples:
389 * (name, sortName (key to the sort array))
390 *
391 * @param string $action
392 * The action being performed.
393 * @param string $output
394 * What should the result set include (web/email/csv).
395 *
396 * @return array
397 * the column headers that need to be displayed
398 */
399 public function &getColumnHeaders($action = NULL, $output = NULL) {
400 if (!isset(self::$_columnHeaders)) {
401 self::$_columnHeaders = [
402 [
403 'name' => ts('Type'),
404 'sort' => 'activity_type_id',
405 'direction' => CRM_Utils_Sort::DONTCARE,
406 ],
407 [
408 'name' => ts('Subject'),
409 'sort' => 'activity_subject',
410 'direction' => CRM_Utils_Sort::DONTCARE,
411 ],
412 [
413 'name' => ts('Added By'),
414 'sort' => 'source_contact',
415 'direction' => CRM_Utils_Sort::DONTCARE,
416 ],
417 ['name' => ts('With')],
418 ['name' => ts('Assigned')],
419 [
420 'name' => ts('Date'),
421 'sort' => 'activity_date_time',
422 'direction' => CRM_Utils_Sort::DESCENDING,
423 ],
424 [
425 'name' => ts('Status'),
426 'sort' => 'activity_status',
427 'direction' => CRM_Utils_Sort::DONTCARE,
428 ],
429 [
430 'desc' => ts('Actions'),
431 ],
432 ];
433 }
434 return self::$_columnHeaders;
435 }
436
437 /**
438 * @return mixed
439 */
440 public function alphabetQuery() {
441 return $this->_query->alphabetQuery();
442 }
443
444 /**
445 * @return \CRM_Contact_BAO_Query
446 */
447 public function &getQuery() {
448 return $this->_query;
449 }
450
451 /**
452 * Name of export file.
453 *
454 * @param string $output
455 * Type of output.
456 *
457 * @return string
458 * name of the file
459 */
460 public function getExportFileName($output = 'csv') {
461 return ts('CiviCRM Activity Search');
462 }
463
464 }