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