Merge pull request #17452 from eileenmcnaughton/dummy_bag
[civicrm-core.git] / CRM / Mailing / BAO / Query.php
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 class CRM_Mailing_BAO_Query {
18
19 /**
20 * Get fields for the mailing & mailing job entity.
21 *
22 * @return array
23 */
24 public static function &getFields() {
25 $mailingFields = CRM_Mailing_BAO_Mailing::fields();
26 $mailingJobFields = CRM_Mailing_BAO_MailingJob::fields();
27
28 // In general it's good to return as many fields as could possibly be searched, but
29 // with the limitation that if the fields do not have unique names they might
30 // clobber other fields :-(
31 $fields = [
32 'mailing_id' => $mailingFields['id'],
33 'mailing_job_start_date' => $mailingJobFields['mailing_job_start_date'],
34 ];
35 return $fields;
36 }
37
38 /**
39 * If mailings are involved, add the specific Mailing fields
40 *
41 * @param $query
42 */
43 public static function select(&$query) {
44 // if Mailing mode add mailing id
45 if ($query->_mode & CRM_Contact_BAO_Query::MODE_MAILING) {
46 $query->_select['mailing_id'] = "civicrm_mailing.id as mailing_id";
47 $query->_element['mailing_id'] = 1;
48
49 // base table is contact, so join recipients to it
50 $query->_tables['civicrm_mailing_recipients'] = $query->_whereTables['civicrm_mailing_recipients']
51 = " INNER JOIN civicrm_mailing_recipients ON civicrm_mailing_recipients.contact_id = contact_a.id ";
52
53 $query->_tables['civicrm_mailing'] = $query->_whereTables['civicrm_mailing'] = 1;
54
55 // get mailing name
56 if (!empty($query->_returnProperties['mailing_name'])) {
57 $query->_select['mailing_name'] = "civicrm_mailing.name as mailing_name";
58 $query->_element['mailing_name'] = 1;
59 }
60
61 // get mailing subject
62 if (!empty($query->_returnProperties['mailing_subject'])) {
63 $query->_select['mailing_subject'] = "civicrm_mailing.subject as mailing_subject";
64 $query->_element['mailing_subject'] = 1;
65 }
66
67 // get mailing status
68 if (!empty($query->_returnProperties['mailing_job_status'])) {
69 $query->_tables['civicrm_mailing_job'] = $query->_whereTables['civicrm_mailing_job']
70 = " LEFT JOIN civicrm_mailing_job ON civicrm_mailing_job.mailing_id = civicrm_mailing.id AND civicrm_mailing_job.parent_id IS NULL AND civicrm_mailing_job.is_test != 1 ";
71 $query->_select['mailing_job_status'] = "civicrm_mailing_job.status as mailing_job_status";
72 $query->_element['mailing_job_status'] = 1;
73 }
74
75 // get email on hold
76 if (!empty($query->_returnProperties['email_on_hold'])) {
77 $query->_select['email_on_hold'] = "recipient_email.on_hold as email_on_hold";
78 $query->_element['email_on_hold'] = 1;
79 $query->_tables['recipient_email'] = $query->_whereTables['recipient_email'] = 1;
80 }
81
82 // get recipient email
83 if (!empty($query->_returnProperties['email'])) {
84 $query->_select['email'] = "recipient_email.email as email";
85 $query->_element['email'] = 1;
86 $query->_tables['recipient_email'] = $query->_whereTables['recipient_email'] = 1;
87 }
88
89 // get user opt out
90 if (!empty($query->_returnProperties['contact_opt_out'])) {
91 $query->_select['contact_opt_out'] = "contact_a.is_opt_out as contact_opt_out";
92 $query->_element['contact_opt_out'] = 1;
93 }
94
95 // mailing job end date / completed date
96 if (!empty($query->_returnProperties['mailing_job_end_date'])) {
97 $query->_tables['civicrm_mailing_job'] = $query->_whereTables['civicrm_mailing_job']
98 = " LEFT JOIN civicrm_mailing_job ON civicrm_mailing_job.mailing_id = civicrm_mailing.id AND civicrm_mailing_job.parent_id IS NULL AND civicrm_mailing_job.is_test != 1 ";
99 $query->_select['mailing_job_end_date'] = "civicrm_mailing_job.end_date as mailing_job_end_date";
100 $query->_element['mailing_job_end_date'] = 1;
101 }
102
103 if (!empty($query->_returnProperties['mailing_recipients_id'])) {
104 $query->_select['mailing_recipients_id'] = " civicrm_mailing_recipients.id as mailing_recipients_id";
105 $query->_element['mailing_recipients_id'] = 1;
106 }
107 }
108
109 if (!empty($query->_returnProperties['mailing_campaign_id'])) {
110 $query->_select['mailing_campaign_id'] = 'civicrm_mailing.campaign_id as mailing_campaign_id';
111 $query->_element['mailing_campaign_id'] = 1;
112 $query->_tables['civicrm_campaign'] = 1;
113 }
114 }
115
116 /**
117 * Get the metadata for fields to be included on the mailing search form.
118 *
119 * @throws \CiviCRM_API3_Exception
120 *
121 * @todo ideally this would be a trait included on the mailing search & advanced search
122 * rather than a static function.
123 */
124 public static function getSearchFieldMetadata() {
125 $fields = ['mailing_job_start_date', 'is_archived'];
126 $metadata = civicrm_api3('Mailing', 'getfields', [])['values'];
127 $metadata = array_merge($metadata, civicrm_api3('MailingJob', 'getfields', [])['values']);
128 return array_intersect_key($metadata, array_flip($fields));
129 }
130
131 /**
132 * @param $query
133 */
134 public static function where(&$query) {
135 $grouping = NULL;
136 foreach (array_keys($query->_params) as $id) {
137 if (empty($query->_params[$id][0])) {
138 continue;
139 }
140 if (substr($query->_params[$id][0], 0, 8) == 'mailing_') {
141 if ($query->_mode == CRM_Contact_BAO_Query::MODE_CONTACTS) {
142 $query->_useDistinct = TRUE;
143 }
144 $grouping = $query->_params[$id][3];
145 self::whereClauseSingle($query->_params[$id], $query);
146 }
147 }
148 }
149
150 /**
151 * @param string $name
152 * @param $mode
153 * @param $side
154 *
155 * @return null|string
156 */
157 public static function from($name, $mode, $side) {
158 $from = NULL;
159
160 switch ($name) {
161 case 'civicrm_mailing_recipients':
162 $from = " $side JOIN civicrm_mailing_recipients ON civicrm_mailing_recipients.contact_id = contact_a.id";
163 break;
164
165 case 'civicrm_mailing_event_queue':
166 // this is tightly binded so as to do a check WRT actual job recipients ('child' type jobs)
167 $from = " INNER JOIN civicrm_mailing_event_queue ON
168 civicrm_mailing_event_queue.contact_id = civicrm_mailing_recipients.contact_id
169 AND civicrm_mailing_event_queue.job_id = civicrm_mailing_job.id AND civicrm_mailing_job.job_type = 'child'";
170 break;
171
172 case 'civicrm_mailing':
173 $from = " $side JOIN civicrm_mailing ON civicrm_mailing.id = civicrm_mailing_recipients.mailing_id ";
174 break;
175
176 case 'civicrm_mailing_job':
177 $from = " $side JOIN civicrm_mailing_job ON civicrm_mailing_job.mailing_id = civicrm_mailing.id AND civicrm_mailing_job.is_test != 1 ";
178 break;
179
180 case 'civicrm_mailing_event_bounce':
181 case 'civicrm_mailing_event_delivered':
182 case 'civicrm_mailing_event_opened':
183 case 'civicrm_mailing_event_reply':
184 case 'civicrm_mailing_event_unsubscribe':
185 case 'civicrm_mailing_event_forward':
186 case 'civicrm_mailing_event_trackable_url_open':
187 $from = " $side JOIN $name ON $name.event_queue_id = civicrm_mailing_event_queue.id";
188 break;
189
190 case 'recipient_email':
191 $from = " $side JOIN civicrm_email recipient_email ON recipient_email.id = civicrm_mailing_recipients.email_id";
192 break;
193
194 case 'civicrm_campaign':
195 $from = " $side JOIN civicrm_campaign ON civicrm_campaign.id = civicrm_mailing.campaign_id";
196 break;
197 }
198
199 return $from;
200 }
201
202 /**
203 * @param $mode
204 * @param bool $includeCustomFields
205 *
206 * @return array|null
207 */
208 public static function defaultReturnProperties(
209 $mode,
210 $includeCustomFields = TRUE
211 ) {
212
213 $properties = NULL;
214 if ($mode & CRM_Contact_BAO_Query::MODE_MAILING) {
215 $properties = [
216 'mailing_id' => 1,
217 'mailing_campaign_id' => 1,
218 'mailing_name' => 1,
219 'sort_name' => 1,
220 'email' => 1,
221 'mailing_subject' => 1,
222 'email_on_hold' => 1,
223 'contact_opt_out' => 1,
224 'mailing_job_status' => 1,
225 'mailing_job_end_date' => 1,
226 'contact_type' => 1,
227 'contact_sub_type' => 1,
228 'mailing_recipients_id' => 1,
229 ];
230 }
231 return $properties;
232 }
233
234 /**
235 * @param $values
236 * @param $query
237 */
238 public static function whereClauseSingle(&$values, &$query) {
239 list($name, $op, $value, $grouping, $wildcard) = $values;
240
241 switch ($name) {
242 case 'mailing_id':
243 $selectedMailings = array_flip($value);
244 $value = "(" . implode(',', $value) . ")";
245 $op = 'IN';
246 $query->_where[$grouping][] = "civicrm_mailing.id $op $value";
247
248 $mailings = CRM_Mailing_BAO_Mailing::getMailingsList();
249 foreach ($selectedMailings as $id => $dnc) {
250 $selectedMailings[$id] = $mailings[$id];
251 }
252 $selectedMailings = implode(' or ', $selectedMailings);
253
254 $query->_qill[$grouping][] = "Mailing Name $op \"$selectedMailings\"";
255 $query->_tables['civicrm_mailing'] = $query->_whereTables['civicrm_mailing'] = 1;
256 $query->_tables['civicrm_mailing_recipients'] = $query->_whereTables['civicrm_mailing_recipients'] = 1;
257 return;
258
259 case 'mailing_name':
260 $value = addslashes($value);
261 if ($wildcard) {
262 $value = "%$value%";
263 $op = 'LIKE';
264 }
265
266 $query->_where[$grouping][] = "civicrm_mailing.name $op '$value'";
267 $query->_qill[$grouping][] = "Mailing Namename $op \"$value\"";
268 $query->_tables['civicrm_mailing'] = $query->_whereTables['civicrm_mailing'] = 1;
269 $query->_tables['civicrm_mailing_recipients'] = $query->_whereTables['civicrm_mailing_recipients'] = 1;
270 return;
271
272 case 'mailing_date':
273 case 'mailing_date_low':
274 case 'mailing_date_high':
275 $query->_tables['civicrm_mailing_job'] = $query->_whereTables['civicrm_mailing_job'] = 1;
276 $query->dateQueryBuilder($values,
277 'civicrm_mailing_job', 'mailing_date', 'start_date', 'Mailing Delivery Date'
278 );
279 return;
280
281 case 'mailing_delivery_status':
282 $options = CRM_Mailing_PseudoConstant::yesNoOptions('delivered');
283
284 list($name, $op, $value, $grouping, $wildcard) = $values;
285 if ($value == 'Y') {
286 self::mailingEventQueryBuilder($query, $values,
287 'civicrm_mailing_event_delivered',
288 'mailing_delivery_status',
289 ts('Mailing Delivery'),
290 $options
291 );
292 }
293 elseif ($value == 'N') {
294 $options['Y'] = $options['N'];
295 $values = [$name, $op, 'Y', $grouping, $wildcard];
296 self::mailingEventQueryBuilder($query, $values,
297 'civicrm_mailing_event_bounce',
298 'mailing_delivery_status',
299 ts('Mailing Delivery'),
300 $options
301 );
302 }
303 return;
304
305 case 'mailing_bounce_types':
306 $op = 'IN';
307 $values = [$name, $op, $value, $grouping, $wildcard];
308 self::mailingEventQueryBuilder($query, $values,
309 'civicrm_mailing_event_bounce',
310 'bounce_type_id',
311 ts('Bounce type(s)'),
312 CRM_Core_PseudoConstant::get('CRM_Mailing_Event_DAO_Bounce', 'bounce_type_id', [
313 'keyColumn' => 'id',
314 'labelColumn' => 'name',
315 ])
316 );
317 return;
318
319 case 'mailing_open_status':
320 self::mailingEventQueryBuilder($query, $values,
321 'civicrm_mailing_event_opened', 'mailing_open_status', ts('Mailing: Trackable Opens'), CRM_Mailing_PseudoConstant::yesNoOptions('open')
322 );
323 return;
324
325 case 'mailing_click_status':
326 self::mailingEventQueryBuilder($query, $values,
327 'civicrm_mailing_event_trackable_url_open', 'mailing_click_status', ts('Mailing: Trackable URL Clicks'), CRM_Mailing_PseudoConstant::yesNoOptions('click')
328 );
329 return;
330
331 case 'mailing_reply_status':
332 self::mailingEventQueryBuilder($query, $values,
333 'civicrm_mailing_event_reply', 'mailing_reply_status', ts('Mailing: Trackable Replies'), CRM_Mailing_PseudoConstant::yesNoOptions('reply')
334 );
335 return;
336
337 case 'mailing_optout':
338 $valueTitle = [1 => ts('Opt-out Requests')];
339 // include opt-out events only
340 $query->_where[$grouping][] = "civicrm_mailing_event_unsubscribe.org_unsubscribe = 1";
341 self::mailingEventQueryBuilder($query, $values,
342 'civicrm_mailing_event_unsubscribe', 'mailing_unsubscribe',
343 ts('Mailing: '), $valueTitle
344 );
345 return;
346
347 case 'mailing_unsubscribe':
348 $valueTitle = [1 => ts('Unsubscribe Requests')];
349 // exclude opt-out events
350 $query->_where[$grouping][] = "civicrm_mailing_event_unsubscribe.org_unsubscribe = 0";
351 self::mailingEventQueryBuilder($query, $values,
352 'civicrm_mailing_event_unsubscribe', 'mailing_unsubscribe',
353 ts('Mailing: '), $valueTitle
354 );
355 return;
356
357 case 'mailing_forward':
358 $valueTitle = ['Y' => ts('Forwards')];
359 // since its a checkbox
360 $values[2] = 'Y';
361 self::mailingEventQueryBuilder($query, $values,
362 'civicrm_mailing_event_forward', 'mailing_forward',
363 ts('Mailing: '), $valueTitle
364 );
365 return;
366
367 case 'mailing_job_status':
368 if (!empty($value)) {
369 if ($value != 'Scheduled' && $value != 'Canceled') {
370 $query->_tables['civicrm_mailing_event_queue'] = $query->_whereTables['civicrm_mailing_event_queue'] = 1;
371 }
372 $query->_tables['civicrm_mailing_job'] = $query->_whereTables['civicrm_mailing_job'] = 1;
373 $query->_where[$grouping][] = " civicrm_mailing_job.status = '{$value}' ";
374 $query->_qill[$grouping][] = "Mailing Job Status IS \"$value\"";
375 }
376 return;
377
378 case 'mailing_campaign_id':
379 $name = 'campaign_id';
380 $query->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause("civicrm_mailing.$name", $op, $value, 'Integer');
381 list($op, $value) = CRM_Contact_BAO_Query::buildQillForFieldValue('CRM_Mailing_DAO_Mailing', $name, $value, $op);
382 $query->_qill[$grouping][] = ts('Campaign %1 %2', [1 => $op, 2 => $value]);
383 $query->_tables['civicrm_mailing'] = $query->_whereTables['civicrm_mailing'] = 1;
384 $query->_tables['civicrm_mailing_recipients'] = $query->_whereTables['civicrm_mailing_recipients'] = 1;
385 return;
386 }
387 }
388
389 /**
390 * Add all the elements shared between Mailing search and advnaced search.
391 *
392 * @param \CRM_Mailing_Form_Search $form
393 *
394 * @throws \CiviCRM_API3_Exception
395 */
396 public static function buildSearchForm(&$form) {
397 $form->addSearchFieldMetadata(['Mailing' => self::getSearchFieldMetadata()]);
398 $form->addFormFieldsFromMetadata();
399
400 // mailing selectors
401 $mailings = CRM_Mailing_BAO_Mailing::getMailingsList();
402
403 if (!empty($mailings)) {
404 $form->add('select', 'mailing_id', ts('Mailing Name(s)'), $mailings, FALSE,
405 ['id' => 'mailing_id', 'multiple' => 'multiple', 'class' => 'crm-select2']
406 );
407 }
408
409 $mailingJobStatuses = [
410 '' => ts('- select -'),
411 'Complete' => 'Complete',
412 'Scheduled' => 'Scheduled',
413 'Running' => 'Running',
414 'Canceled' => 'Canceled',
415 ];
416 $form->addElement('select', 'mailing_job_status', ts('Mailing Job Status'), $mailingJobStatuses, FALSE);
417
418 $mailingBounceTypes = CRM_Core_PseudoConstant::get(
419 'CRM_Mailing_Event_DAO_Bounce', 'bounce_type_id',
420 ['keyColumn' => 'id', 'labelColumn' => 'name']
421 );
422 $form->add('select', 'mailing_bounce_types', ts('Bounce Types'), $mailingBounceTypes, FALSE,
423 ['id' => 'mailing_bounce_types', 'multiple' => 'multiple', 'class' => 'crm-select2']
424 );
425
426 // event filters
427 $form->addRadio('mailing_delivery_status', ts('Delivery Status'), CRM_Mailing_PseudoConstant::yesNoOptions('delivered'), ['allowClear' => TRUE]);
428 $form->addRadio('mailing_open_status', ts('Trackable Opens'), CRM_Mailing_PseudoConstant::yesNoOptions('open'), ['allowClear' => TRUE]);
429 $form->addRadio('mailing_click_status', ts('Trackable URLs'), CRM_Mailing_PseudoConstant::yesNoOptions('click'), ['allowClear' => TRUE]);
430 $form->addRadio('mailing_reply_status', ts('Trackable Replies'), CRM_Mailing_PseudoConstant::yesNoOptions('reply'), ['allowClear' => TRUE]);
431
432 $form->add('checkbox', 'mailing_unsubscribe', ts('Unsubscribe Requests'));
433 $form->add('checkbox', 'mailing_optout', ts('Opt-out Requests'));
434 $form->add('checkbox', 'mailing_forward', ts('Forwards'));
435 // Campaign select field
436 CRM_Campaign_BAO_Campaign::addCampaignInComponentSearch($form, 'mailing_campaign_id');
437
438 $form->assign('validCiviMailing', TRUE);
439 }
440
441 /**
442 * @param $row
443 * @param int $id
444 */
445 public static function searchAction(&$row, $id) {
446 }
447
448 /**
449 * @param $tables
450 */
451 public static function tableNames(&$tables) {
452 if (isset($tables['civicrm_mailing_job'])) {
453 $tables['civicrm_mailing'] = $tables['civicrm_mailing'] ?? 1;
454 $tables['civicrm_mailing_recipients'] = $tables['civicrm_mailing_recipients'] ?? 1;
455 }
456 }
457
458 /**
459 * Filter query results based on which contacts do (not) have a particular mailing event in their history.
460 *
461 * @param $query
462 * @param $values
463 * @param string $tableName
464 * @param string $fieldName
465 * @param $fieldTitle
466 *
467 * @param $valueTitles
468 */
469 public static function mailingEventQueryBuilder(&$query, &$values, $tableName, $fieldName, $fieldTitle, &$valueTitles) {
470 list($name, $op, $value, $grouping, $wildcard) = $values;
471
472 if (empty($value) || $value == 'A') {
473 // don't do any filtering
474 return;
475 }
476
477 if ($value == 'Y') {
478 $query->_where[$grouping][] = $tableName . ".id is not null ";
479 }
480 elseif ($value == 'N') {
481 $query->_where[$grouping][] = $tableName . ".id is null ";
482 }
483
484 if (is_array($value)) {
485 $query->_where[$grouping][] = "$tableName.$fieldName $op (" . implode(',', $value) . ")";
486 $query->_qill[$grouping][] = "$fieldTitle $op " . implode(', ', array_intersect_key($valueTitles, array_flip($value)));
487 }
488 else {
489 $query->_qill[$grouping][] = $fieldTitle . ' - ' . $valueTitles[$value];
490 }
491
492 $query->_tables['civicrm_mailing'] = $query->_whereTables['civicrm_mailing'] = 1;
493 $query->_tables['civicrm_mailing_job'] = $query->_whereTables['civicrm_mailing_job'] = 1;
494 $query->_tables['civicrm_mailing_event_queue'] = $query->_whereTables['civicrm_mailing_event_queue'] = 1;
495 $query->_tables['civicrm_mailing_recipients'] = $query->_whereTables['civicrm_mailing_recipients'] = 1;
496 $query->_tables[$tableName] = $query->_whereTables[$tableName] = 1;
497 }
498
499 }