3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Mailing_BAO_Query
{
20 * Get fields for the mailing & mailing job entity.
24 public static function &getFields() {
25 $mailingFields = CRM_Mailing_BAO_Mailing
::fields();
26 $mailingJobFields = CRM_Mailing_BAO_MailingJob
::fields();
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 :-(
32 'mailing_id' => $mailingFields['id'],
33 'mailing_job_start_date' => $mailingJobFields['mailing_job_start_date'],
39 * If mailings are involved, add the specific Mailing fields
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;
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 ";
53 $query->_tables
['civicrm_mailing'] = $query->_whereTables
['civicrm_mailing'] = 1;
56 if (!empty($query->_returnProperties
['mailing_name'])) {
57 $query->_select
['mailing_name'] = "civicrm_mailing.name as mailing_name";
58 $query->_element
['mailing_name'] = 1;
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;
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;
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;
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;
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;
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;
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;
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;
117 * Get the metadata for fields to be included on the mailing search form.
119 * @throws \CiviCRM_API3_Exception
121 * @todo ideally this would be a trait included on the mailing search & advanced search
122 * rather than a static function.
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));
134 public static function where(&$query) {
136 foreach (array_keys($query->_params
) as $id) {
137 if (empty($query->_params
[$id][0])) {
140 if (substr($query->_params
[$id][0], 0, 8) == 'mailing_') {
141 if ($query->_mode
== CRM_Contact_BAO_Query
::MODE_CONTACTS
) {
142 $query->_useDistinct
= TRUE;
144 $grouping = $query->_params
[$id][3];
145 self
::whereClauseSingle($query->_params
[$id], $query);
151 * @param string $name
155 * @return null|string
157 public static function from($name, $mode, $side) {
161 case 'civicrm_mailing_recipients':
162 $from = " $side JOIN civicrm_mailing_recipients ON civicrm_mailing_recipients.contact_id = contact_a.id";
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'";
172 case 'civicrm_mailing':
173 $from = " $side JOIN civicrm_mailing ON civicrm_mailing.id = civicrm_mailing_recipients.mailing_id ";
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 ";
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";
190 case 'recipient_email':
191 $from = " $side JOIN civicrm_email recipient_email ON recipient_email.id = civicrm_mailing_recipients.email_id";
194 case 'civicrm_campaign':
195 $from = " $side JOIN civicrm_campaign ON civicrm_campaign.id = civicrm_mailing.campaign_id";
204 * @param bool $includeCustomFields
208 public static function defaultReturnProperties(
210 $includeCustomFields = TRUE
214 if ($mode & CRM_Contact_BAO_Query
::MODE_MAILING
) {
217 'mailing_campaign_id' => 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,
227 'contact_sub_type' => 1,
228 'mailing_recipients_id' => 1,
238 public static function whereClauseSingle(&$values, &$query) {
239 list($name, $op, $value, $grouping, $wildcard) = $values;
243 $selectedMailings = array_flip($value);
244 $value = "(" . implode(',', $value) . ")";
246 $query->_where
[$grouping][] = "civicrm_mailing.id $op $value";
248 $mailings = CRM_Mailing_BAO_Mailing
::getMailingsList();
249 foreach ($selectedMailings as $id => $dnc) {
250 $selectedMailings[$id] = $mailings[$id];
252 $selectedMailings = implode(' or ', $selectedMailings);
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;
260 $value = addslashes($value);
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;
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'
281 case 'mailing_delivery_status':
282 $options = CRM_Mailing_PseudoConstant
::yesNoOptions('delivered');
284 list($name, $op, $value, $grouping, $wildcard) = $values;
286 self
::mailingEventQueryBuilder($query, $values,
287 'civicrm_mailing_event_delivered',
288 'mailing_delivery_status',
289 ts('Mailing Delivery'),
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'),
305 case 'mailing_bounce_types':
307 $values = [$name, $op, $value, $grouping, $wildcard];
308 self
::mailingEventQueryBuilder($query, $values,
309 'civicrm_mailing_event_bounce',
311 ts('Bounce type(s)'),
312 CRM_Core_PseudoConstant
::get('CRM_Mailing_Event_DAO_Bounce', 'bounce_type_id', [
314 'labelColumn' => 'name',
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')
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')
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')
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
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
357 case 'mailing_forward':
358 $valueTitle = ['Y' => ts('Forwards')];
359 // since its a checkbox
361 self
::mailingEventQueryBuilder($query, $values,
362 'civicrm_mailing_event_forward', 'mailing_forward',
363 ts('Mailing: '), $valueTitle
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;
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\"";
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;
390 * Add all the elements shared between Mailing search and advnaced search.
392 * @param \CRM_Mailing_Form_Search $form
394 * @throws \CiviCRM_API3_Exception
396 public static function buildSearchForm(&$form) {
397 $form->addSearchFieldMetadata(['Mailing' => self
::getSearchFieldMetadata()]);
398 $form->addFormFieldsFromMetadata();
401 $mailings = CRM_Mailing_BAO_Mailing
::getMailingsList();
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']
409 $mailingJobStatuses = [
410 '' => ts('- select -'),
411 'Complete' => 'Complete',
412 'Scheduled' => 'Scheduled',
413 'Running' => 'Running',
414 'Canceled' => 'Canceled',
416 $form->addElement('select', 'mailing_job_status', ts('Mailing Job Status'), $mailingJobStatuses, FALSE);
418 $mailingBounceTypes = CRM_Core_PseudoConstant
::get(
419 'CRM_Mailing_Event_DAO_Bounce', 'bounce_type_id',
420 ['keyColumn' => 'id', 'labelColumn' => 'name']
422 $form->add('select', 'mailing_bounce_types', ts('Bounce Types'), $mailingBounceTypes, FALSE,
423 ['id' => 'mailing_bounce_types', 'multiple' => 'multiple', 'class' => 'crm-select2']
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]);
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');
438 $form->assign('validCiviMailing', TRUE);
445 public static function searchAction(&$row, $id) {
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;
459 * Filter query results based on which contacts do (not) have a particular mailing event in their history.
463 * @param string $tableName
464 * @param string $fieldName
467 * @param $valueTitles
469 public static function mailingEventQueryBuilder(&$query, &$values, $tableName, $fieldName, $fieldTitle, &$valueTitles) {
470 list($name, $op, $value, $grouping, $wildcard) = $values;
472 if (empty($value) ||
$value == 'A') {
473 // don't do any filtering
478 $query->_where
[$grouping][] = $tableName . ".id is not null ";
480 elseif ($value == 'N') {
481 $query->_where
[$grouping][] = $tableName . ".id is null ";
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)));
489 $query->_qill
[$grouping][] = $fieldTitle . ' - ' . $valueTitles[$value];
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;