Merge pull request #15838 from demeritcowboy/getcasereport-split
[civicrm-core.git] / CRM / Mailing / Event / BAO / Unsubscribe.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
18 require_once 'Mail/mime.php';
19
20 /**
21 * Class CRM_Mailing_Event_BAO_Unsubscribe
22 */
23 class CRM_Mailing_Event_BAO_Unsubscribe extends CRM_Mailing_Event_DAO_Unsubscribe {
24
25 /**
26 * Class constructor.
27 */
28 public function __construct() {
29 parent::__construct();
30 }
31
32 /**
33 * Unsubscribe a contact from the domain.
34 *
35 * @param int $job_id
36 * The job ID.
37 * @param int $queue_id
38 * The Queue Event ID of the recipient.
39 * @param string $hash
40 * The hash.
41 *
42 * @return bool
43 * Was the contact successfully unsubscribed?
44 */
45 public static function unsub_from_domain($job_id, $queue_id, $hash) {
46 $q = CRM_Mailing_Event_BAO_Queue::verify($job_id, $queue_id, $hash);
47 if (!$q) {
48 return FALSE;
49 }
50
51 $transaction = new CRM_Core_Transaction();
52
53 $now = date('YmdHis');
54 if (CRM_Core_BAO_Email::isMultipleBulkMail()) {
55 $email = new CRM_Core_BAO_Email();
56 $email->id = $q->email_id;
57 if ($email->find(TRUE)) {
58 $sql = "
59 UPDATE civicrm_email
60 SET on_hold = 2,
61 hold_date = %1
62 WHERE email = %2
63 ";
64 $sqlParams = [
65 1 => [$now, 'Timestamp'],
66 2 => [$email->email, 'String'],
67 ];
68 CRM_Core_DAO::executeQuery($sql, $sqlParams);
69 }
70 }
71 else {
72 $contact = new CRM_Contact_BAO_Contact();
73 $contact->id = $q->contact_id;
74 $contact->is_opt_out = TRUE;
75 $contact->save();
76 }
77
78 $ue = new CRM_Mailing_Event_BAO_Unsubscribe();
79 $ue->event_queue_id = $queue_id;
80 $ue->org_unsubscribe = 1;
81 $ue->time_stamp = $now;
82 $ue->save();
83
84 $shParams = [
85 'contact_id' => $q->contact_id,
86 'group_id' => NULL,
87 'status' => 'Removed',
88 'method' => 'Email',
89 'tracking' => $ue->id,
90 ];
91 CRM_Contact_BAO_SubscriptionHistory::create($shParams);
92
93 $transaction->commit();
94
95 return TRUE;
96 }
97
98 /**
99 * Unsubscribe a contact from all groups that received this mailing.
100 *
101 * @param int $job_id
102 * The job ID.
103 * @param int $queue_id
104 * The Queue Event ID of the recipient.
105 * @param string $hash
106 * The hash.
107 * @param bool $return
108 * If true return the list of groups.
109 *
110 * @return array|null
111 * $groups Array of all groups from which the contact was removed, or null if the queue event could not be found.
112 *
113 * @throws \CiviCRM_API3_Exception
114 */
115 public static function &unsub_from_mailing($job_id, $queue_id, $hash, $return = FALSE) {
116 // First make sure there's a matching queue event.
117
118 $q = CRM_Mailing_Event_BAO_Queue::verify($job_id, $queue_id, $hash);
119 $success = NULL;
120 if (!$q) {
121 return $success;
122 }
123
124 $contact_id = $q->contact_id;
125 $transaction = new CRM_Core_Transaction();
126
127 $do = new CRM_Core_DAO();
128 $mgObject = new CRM_Mailing_DAO_MailingGroup();
129 $mg = $mgObject->getTableName();
130 $jobObject = new CRM_Mailing_BAO_MailingJob();
131 $job = $jobObject->getTableName();
132 $mailingObject = new CRM_Mailing_BAO_Mailing();
133 $mailing = $mailingObject->getTableName();
134 $groupObject = new CRM_Contact_BAO_Group();
135 $group = $groupObject->getTableName();
136 $gcObject = new CRM_Contact_BAO_GroupContact();
137 $gc = $gcObject->getTableName();
138 $abObject = new CRM_Mailing_DAO_MailingAB();
139 $ab = $abObject->getTableName();
140
141 $mailing_id = civicrm_api3('MailingJob', 'getvalue', ['id' => $job_id, 'return' => 'mailing_id']);
142 $mailing_type = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Mailing', $mailing_id, 'mailing_type', 'id');
143 $entity = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingGroup', $mailing_id, 'entity_table', 'mailing_id');
144
145 // If $entity is null and $mailing_Type is either winner or experiment then we are deailing with an AB test
146 $abtest_types = ['experiment', 'winner'];
147 if (empty($entity) && in_array($mailing_type, $abtest_types)) {
148 $mailing_id_a = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingAB', $mailing_id, 'mailing_id_a', 'mailing_id_b');
149 $field = 'mailing_id_b';
150 if (empty($mailing_id_a)) {
151 $mailing_id_a = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingAB', $mailing_id, 'mailing_id_a', 'mailing_id_c');
152 $field = 'mailing_id_c';
153 }
154 $jobJoin = "INNER JOIN $ab ON $ab.mailing_id_a = $mg.mailing_id
155 INNER JOIN $job ON $job.mailing_id = $ab.$field";
156 $entity = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingGroup', $mailing_id_a, 'entity_table', 'mailing_id');
157 }
158 else {
159 $jobJoin = "INNER JOIN $job ON $job.mailing_id = $mg.mailing_id";
160 }
161
162 $groupClause = '';
163 if ($entity == $group) {
164 $groupClause = "AND $group.is_hidden = 0";
165 }
166
167 $do->query("
168 SELECT $mg.entity_table as entity_table,
169 $mg.entity_id as entity_id,
170 $mg.group_type as group_type
171 FROM $mg
172 $jobJoin
173 INNER JOIN $entity
174 ON $mg.entity_id = $entity.id
175 WHERE $job.id = " . CRM_Utils_Type::escape($job_id, 'Integer') . "
176 AND $mg.group_type IN ('Include', 'Base') $groupClause"
177 );
178
179 // Make a list of groups and a list of prior mailings that received
180 // this mailing.
181
182 $groups = [];
183 $base_groups = [];
184 $mailings = [];
185
186 while ($do->fetch()) {
187 if ($do->entity_table == $group) {
188 if ($do->group_type == 'Base') {
189 $base_groups[$do->entity_id] = NULL;
190 }
191 else {
192 $groups[$do->entity_id] = NULL;
193 }
194 }
195 elseif ($do->entity_table == $mailing) {
196 $mailings[] = $do->entity_id;
197 }
198 }
199
200 // As long as we have prior mailings, find their groups and add to the
201 // list.
202
203 while (!empty($mailings)) {
204 $do = CRM_Core_DAO::executeQuery("
205 SELECT $mg.entity_table as entity_table,
206 $mg.entity_id as entity_id
207 FROM civicrm_mailing_group $mg
208 WHERE $mg.mailing_id IN (" . implode(', ', $mailings) . ")
209 AND $mg.group_type = 'Include'");
210
211 $mailings = [];
212
213 while ($do->fetch()) {
214 if ($do->entity_table == $group) {
215 $groups[$do->entity_id] = TRUE;
216 }
217 elseif ($do->entity_table == $mailing) {
218 $mailings[] = $do->entity_id;
219 }
220 }
221 }
222
223 //Pass the groups to be unsubscribed from through a hook.
224 $groupIds = array_keys($groups);
225 //include child groups if any
226 $groupIds = array_merge($groupIds, CRM_Contact_BAO_Group::getChildGroupIds($groupIds));
227
228 $baseGroupIds = array_keys($base_groups);
229 CRM_Utils_Hook::unsubscribeGroups('unsubscribe', $mailing_id, $contact_id, $groupIds, $baseGroupIds);
230
231 // Now we have a complete list of recipient groups. Filter out all
232 // those except smart groups, those that the contact belongs to and
233 // base groups from search based mailings.
234 $baseGroupClause = '';
235 if (!empty($baseGroupIds)) {
236 $baseGroupClause = "OR $group.id IN(" . implode(', ', $baseGroupIds) . ")";
237 }
238 $groupIdClause = '';
239 if ($groupIds || $baseGroupIds) {
240 $groupIdClause = "AND $group.id IN (" . implode(', ', array_merge($groupIds, $baseGroupIds)) . ")";
241 }
242 $do = CRM_Core_DAO::executeQuery("
243 SELECT $group.id as group_id,
244 $group.title as title,
245 $group.description as description
246 FROM civicrm_group $group
247 LEFT JOIN civicrm_group_contact $gc
248 ON $gc.group_id = $group.id
249 WHERE $group.is_hidden = 0
250 $groupIdClause
251 AND ($group.saved_search_id is not null
252 OR ($gc.contact_id = $contact_id
253 AND $gc.status = 'Added')
254 $baseGroupClause
255 )");
256
257 if ($return) {
258 $returnGroups = [];
259 while ($do->fetch()) {
260 $returnGroups[$do->group_id] = [
261 'title' => $do->title,
262 'description' => $do->description,
263 ];
264 }
265 return $returnGroups;
266 }
267 else {
268 while ($do->fetch()) {
269 $groups[$do->group_id] = $do->title;
270 }
271 }
272
273 $contacts = [$contact_id];
274 foreach ($groups as $group_id => $group_name) {
275 $notremoved = FALSE;
276 if ($group_name) {
277 if (in_array($group_id, $baseGroupIds)) {
278 list($total, $removed, $notremoved) = CRM_Contact_BAO_GroupContact::addContactsToGroup($contacts, $group_id, 'Email', 'Removed');
279 }
280 else {
281 list($total, $removed, $notremoved) = CRM_Contact_BAO_GroupContact::removeContactsFromGroup($contacts, $group_id, 'Email');
282 }
283 }
284 if ($notremoved) {
285 unset($groups[$group_id]);
286 }
287 }
288
289 $ue = new CRM_Mailing_Event_BAO_Unsubscribe();
290 $ue->event_queue_id = $queue_id;
291 $ue->org_unsubscribe = 0;
292 $ue->time_stamp = date('YmdHis');
293 $ue->save();
294
295 $transaction->commit();
296 return $groups;
297 }
298
299 /**
300 * Send a response email informing the contact of the groups from which he.
301 * has been unsubscribed.
302 *
303 * @param string $queue_id
304 * The queue event ID.
305 * @param array $groups
306 * List of group IDs.
307 * @param bool $is_domain
308 * Is this domain-level?.
309 * @param int $job
310 * The job ID.
311 */
312 public static function send_unsub_response($queue_id, $groups, $is_domain = FALSE, $job) {
313 $config = CRM_Core_Config::singleton();
314 $domain = CRM_Core_BAO_Domain::getDomain();
315
316 $jobObject = new CRM_Mailing_BAO_MailingJob();
317 $jobTable = $jobObject->getTableName();
318 $mailingObject = new CRM_Mailing_DAO_Mailing();
319 $mailingTable = $mailingObject->getTableName();
320 $contactsObject = new CRM_Contact_DAO_Contact();
321 $contacts = $contactsObject->getTableName();
322 $emailObject = new CRM_Core_DAO_Email();
323 $email = $emailObject->getTableName();
324 $queueObject = new CRM_Mailing_Event_BAO_Queue();
325 $queue = $queueObject->getTableName();
326
327 //get the default domain email address.
328 list($domainEmailName, $domainEmailAddress) = CRM_Core_BAO_Domain::getNameAndEmail();
329
330 $dao = new CRM_Mailing_BAO_Mailing();
331 $dao->query(" SELECT * FROM $mailingTable
332 INNER JOIN $jobTable ON
333 $jobTable.mailing_id = $mailingTable.id
334 WHERE $jobTable.id = $job");
335 $dao->fetch();
336
337 $component = new CRM_Mailing_BAO_MailingComponent();
338
339 if ($is_domain) {
340 $component->id = $dao->optout_id;
341 }
342 else {
343 $component->id = $dao->unsubscribe_id;
344 }
345 $component->find(TRUE);
346
347 $html = $component->body_html;
348 if ($component->body_text) {
349 $text = $component->body_text;
350 }
351 else {
352 $text = CRM_Utils_String::htmlToText($component->body_html);
353 }
354
355 $eq = new CRM_Core_DAO();
356 $eq->query(
357 "SELECT $contacts.preferred_mail_format as format,
358 $contacts.id as contact_id,
359 $email.email as email,
360 $queue.hash as hash
361 FROM $contacts
362 INNER JOIN $queue ON $queue.contact_id = $contacts.id
363 INNER JOIN $email ON $queue.email_id = $email.id
364 WHERE $queue.id = " . CRM_Utils_Type::escape($queue_id, 'Integer')
365 );
366 $eq->fetch();
367
368 if ($groups) {
369 foreach ($groups as $key => $value) {
370 if (!$value) {
371 unset($groups[$key]);
372 }
373 }
374 }
375
376 $message = new Mail_mime("\n");
377
378 list($addresses, $urls) = CRM_Mailing_BAO_Mailing::getVerpAndUrls($job, $queue_id, $eq->hash, $eq->email);
379 $bao = new CRM_Mailing_BAO_Mailing();
380 $bao->body_text = $text;
381 $bao->body_html = $html;
382 $tokens = $bao->getTokens();
383 if ($eq->format == 'HTML' || $eq->format == 'Both') {
384 $html = CRM_Utils_Token::replaceDomainTokens($html, $domain, TRUE, $tokens['html']);
385 $html = CRM_Utils_Token::replaceUnsubscribeTokens($html, $domain, $groups, TRUE, $eq->contact_id, $eq->hash);
386 $html = CRM_Utils_Token::replaceActionTokens($html, $addresses, $urls, TRUE, $tokens['html']);
387 $html = CRM_Utils_Token::replaceMailingTokens($html, $dao, NULL, $tokens['html']);
388 $message->setHTMLBody($html);
389 }
390 if (!$html || $eq->format == 'Text' || $eq->format == 'Both') {
391 $text = CRM_Utils_Token::replaceDomainTokens($text, $domain, FALSE, $tokens['text']);
392 $text = CRM_Utils_Token::replaceUnsubscribeTokens($text, $domain, $groups, FALSE, $eq->contact_id, $eq->hash);
393 $text = CRM_Utils_Token::replaceActionTokens($text, $addresses, $urls, FALSE, $tokens['text']);
394 $text = CRM_Utils_Token::replaceMailingTokens($text, $dao, NULL, $tokens['text']);
395 $message->setTxtBody($text);
396 }
397
398 $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain();
399
400 $headers = [
401 'Subject' => $component->subject,
402 'From' => "\"$domainEmailName\" <" . CRM_Core_BAO_Domain::getNoReplyEmailAddress() . '>',
403 'To' => $eq->email,
404 'Reply-To' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(),
405 'Return-Path' => CRM_Core_BAO_Domain::getNoReplyEmailAddress(),
406 ];
407 CRM_Mailing_BAO_Mailing::addMessageIdHeader($headers, 'u', $job, $queue_id, $eq->hash);
408
409 $b = CRM_Utils_Mail::setMimeParams($message);
410 $h = $message->headers($headers);
411
412 $mailer = \Civi::service('pear_mail');
413
414 if (is_object($mailer)) {
415 $errorScope = CRM_Core_TemporaryErrorScope::ignoreException();
416 $mailer->send($eq->email, $h, $b);
417 unset($errorScope);
418 }
419 }
420
421 /**
422 * Get row count for the event selector.
423 *
424 * @param int $mailing_id
425 * ID of the mailing.
426 * @param int $job_id
427 * Optional ID of a job to filter on.
428 * @param bool $is_distinct
429 * Group by queue ID?.
430 *
431 * @param string $org_unsubscribe
432 *
433 * @param string $toDate
434 *
435 * @return int
436 * Number of rows in result set
437 */
438 public static function getTotalCount(
439 $mailing_id, $job_id = NULL,
440 $is_distinct = FALSE, $org_unsubscribe = NULL, $toDate = NULL
441 ) {
442 $dao = new CRM_Core_DAO();
443
444 $unsub = self::$_tableName;
445 $queueObject = new CRM_Mailing_Event_BAO_Queue();
446 $queue = $queueObject->getTableName();
447 $mailingObject = new CRM_Mailing_BAO_Mailing();
448 $mailing = $mailingObject->getTableName();
449 $jobObject = new CRM_Mailing_BAO_MailingJob();
450 $job = $jobObject->getTableName();
451
452 $query = "
453 SELECT COUNT($unsub.id) as unsubs
454 FROM $unsub
455 INNER JOIN $queue
456 ON $unsub.event_queue_id = $queue.id
457 INNER JOIN $job
458 ON $queue.job_id = $job.id
459 INNER JOIN $mailing
460 ON $job.mailing_id = $mailing.id
461 AND $job.is_test = 0
462 WHERE $mailing.id = " . CRM_Utils_Type::escape($mailing_id, 'Integer');
463
464 if (!empty($toDate)) {
465 $query .= " AND $unsub.time_stamp <= $toDate";
466 }
467
468 if (!empty($job_id)) {
469 $query .= " AND $job.id = " . CRM_Utils_Type::escape($job_id, 'Integer');
470 }
471
472 if ($org_unsubscribe !== NULL) {
473 $query .= " AND $unsub.org_unsubscribe = " . ($org_unsubscribe ? 0 : 1);
474 }
475
476 if ($is_distinct) {
477 $query .= " GROUP BY $queue.id ";
478 }
479
480 $dao->query($query);
481 $dao->fetch();
482 if ($is_distinct) {
483 return $dao->N;
484 }
485 else {
486 return $dao->unsubs ? $dao->unsubs : 0;
487 }
488 }
489
490 /**
491 * Get rows for the event browser.
492 *
493 * @param int $mailing_id
494 * ID of the mailing.
495 * @param int $job_id
496 * Optional ID of the job.
497 * @param bool $is_distinct
498 * Group by queue id?.
499 * @param int $offset
500 * Offset.
501 * @param int $rowCount
502 * Number of rows.
503 * @param array $sort
504 * Sort array.
505 *
506 * @param null $org_unsubscribe
507 * @return array
508 * Result set
509 */
510 public static function &getRows(
511 $mailing_id, $job_id = NULL,
512 $is_distinct = FALSE, $offset = NULL, $rowCount = NULL, $sort = NULL,
513 $org_unsubscribe = NULL
514 ) {
515
516 $dao = new CRM_Core_DAO();
517
518 $unsub = self::$_tableName;
519 $queueObject = new CRM_Mailing_Event_BAO_Queue();
520 $queue = $queueObject->getTableName();
521 $mailingObject = new CRM_Mailing_BAO_Mailing();
522 $mailing = $mailingObject->getTableName();
523 $jobObject = new CRM_Mailing_BAO_MailingJob();
524 $job = $jobObject->getTableName();
525 $contactObject = new CRM_Contact_BAO_Contact();
526 $contact = $contactObject->getTableName();
527 $emailObject = new CRM_Core_BAO_Email();
528 $email = $emailObject->getTableName();
529
530 $query = "
531 SELECT $contact.display_name as display_name,
532 $contact.id as contact_id,
533 $email.email as email,
534 $unsub.time_stamp as date,
535 $unsub.org_unsubscribe as org_unsubscribe
536 FROM $contact
537 INNER JOIN $queue
538 ON $queue.contact_id = $contact.id
539 INNER JOIN $email
540 ON $queue.email_id = $email.id
541 INNER JOIN $unsub
542 ON $unsub.event_queue_id = $queue.id
543 INNER JOIN $job
544 ON $queue.job_id = $job.id
545 INNER JOIN $mailing
546 ON $job.mailing_id = $mailing.id
547 AND $job.is_test = 0
548 WHERE $mailing.id = " . CRM_Utils_Type::escape($mailing_id, 'Integer');
549
550 if (!empty($job_id)) {
551 $query .= " AND $job.id = " . CRM_Utils_Type::escape($job_id, 'Integer');
552 }
553
554 if ($org_unsubscribe !== NULL) {
555 $query .= " AND $unsub.org_unsubscribe = " . ($org_unsubscribe ? 0 : 1);
556 }
557
558 if ($is_distinct) {
559 $query .= " GROUP BY $queue.id, $unsub.time_stamp, $unsub.org_unsubscribe";
560 }
561
562 $orderBy = "sort_name ASC, {$unsub}.time_stamp DESC";
563 if ($sort) {
564 if (is_string($sort)) {
565 $sort = CRM_Utils_Type::escape($sort, 'String');
566 $orderBy = $sort;
567 }
568 else {
569 $orderBy = trim($sort->orderBy());
570 }
571 }
572
573 $query .= " ORDER BY {$orderBy} ";
574
575 if ($offset || $rowCount) {
576 //Added "||$rowCount" to avoid displaying all records on first page
577 $query .= ' LIMIT ' . CRM_Utils_Type::escape($offset, 'Integer') . ', ' . CRM_Utils_Type::escape($rowCount, 'Integer');
578 }
579
580 $dao->query($query);
581
582 $results = [];
583
584 while ($dao->fetch()) {
585 $url = CRM_Utils_System::url('civicrm/contact/view',
586 "reset=1&cid={$dao->contact_id}"
587 );
588 $results[] = [
589 'name' => "<a href=\"$url\">{$dao->display_name}</a>",
590 'email' => $dao->email,
591 // Next value displays in selector under either Unsubscribe OR Optout column header, so always s/b Yes.
592 'unsubOrOptout' => ts('Yes'),
593 'date' => CRM_Utils_Date::customFormat($dao->date),
594 ];
595 }
596 return $results;
597 }
598
599 /**
600 * @param int $queueID
601 *
602 * @return array
603 */
604 public static function getContactInfo($queueID) {
605 $query = "
606 SELECT DISTINCT(civicrm_mailing_event_queue.contact_id) as contact_id,
607 civicrm_contact.display_name as display_name
608 civicrm_email.email as email
609 FROM civicrm_mailing_event_queue,
610 civicrm_contact,
611 civicrm_email
612 WHERE civicrm_mailing_event_queue.contact_id = civicrm_contact.id
613 AND civicrm_mailing_event_queue.email_id = civicrm_email.id
614 AND civicrm_mailing_event_queue.id = " . CRM_Utils_Type::escape($queueID, 'Integer');
615
616 $dao = CRM_Core_DAO::executeQuery($query);
617
618 $displayName = 'Unknown';
619 $email = 'Unknown';
620 if ($dao->fetch()) {
621 $displayName = $dao->display_name;
622 $email = $dao->email;
623 }
624
625 return [$displayName, $email];
626 }
627
628 }