<?php
/*
+--------------------------------------------------------------------+
- | CiviCRM version 5 |
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2020 |
- +--------------------------------------------------------------------+
- | This file is a part of CiviCRM. |
- | |
- | CiviCRM is free software; you can copy, modify, and distribute it |
- | under the terms of the GNU Affero General Public License |
- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
- | |
- | CiviCRM is distributed in the hope that it will be useful, but |
- | WITHOUT ANY WARRANTY; without even the implied warranty of |
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
- | See the GNU Affero General Public License for more details. |
+ | Copyright CiviCRM LLC. All rights reserved. |
| |
- | You should have received a copy of the GNU Affero General Public |
- | License and the CiviCRM Licensing Exception along |
- | with this program; if not, contact CiviCRM LLC |
- | at info[AT]civicrm[DOT]org. If you have questions about the |
- | GNU Affero General Public License or the licensing of CiviCRM, |
- | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
- * @copyright CiviCRM LLC (c) 2004-2020
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
*/
require_once 'Mail/mime.php';
$contact_id = $q->contact_id;
$transaction = new CRM_Core_Transaction();
- $do = new CRM_Core_DAO();
- $mgObject = new CRM_Mailing_DAO_MailingGroup();
- $mg = $mgObject->getTableName();
- $jobObject = new CRM_Mailing_BAO_MailingJob();
- $job = $jobObject->getTableName();
- $mailingObject = new CRM_Mailing_BAO_Mailing();
- $mailing = $mailingObject->getTableName();
- $groupObject = new CRM_Contact_BAO_Group();
- $group = $groupObject->getTableName();
- $gcObject = new CRM_Contact_BAO_GroupContact();
- $gc = $gcObject->getTableName();
- $abObject = new CRM_Mailing_DAO_MailingAB();
- $ab = $abObject->getTableName();
-
$mailing_id = civicrm_api3('MailingJob', 'getvalue', ['id' => $job_id, 'return' => 'mailing_id']);
$mailing_type = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Mailing', $mailing_id, 'mailing_type', 'id');
- $entity = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingGroup', $mailing_id, 'entity_table', 'mailing_id');
- // If $entity is null and $mailing_Type is either winner or experiment then we are deailing with an AB test
- $abtest_types = ['experiment', 'winner'];
- if (empty($entity) && in_array($mailing_type, $abtest_types)) {
- $mailing_id_a = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingAB', $mailing_id, 'mailing_id_a', 'mailing_id_b');
- $field = 'mailing_id_b';
- if (empty($mailing_id_a)) {
+ $groupObject = new CRM_Contact_BAO_Group();
+ $groupTableName = $groupObject->getTableName();
+
+ $mailingObject = new CRM_Mailing_BAO_Mailing();
+ $mailingTableName = $mailingObject->getTableName();
+
+ // We need a mailing id that points to the mailing that defined the recipients.
+ // This is usually just the passed-in mailing_id, however in the case of AB
+ // tests, it's the variant 'A' one.
+ $relevant_mailing_id = $mailing_id;
+
+ // Special case for AB Tests:
+ if (in_array($mailing_type, ['experiement', 'winner'])) {
+ // The mailing belongs to an AB test.
+ // See if we can find an AB test where this is variant B.
+ $mailing_id_a = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingAB', mailing_id, 'mailing_id_a', 'mailing_id_b');
+ if (!empty($mailing_id_a)) {
+ // OK, we were given mailing B and we looked up variant A which is the relevant one.
+ $relevant_mailing_id = $mailing_id_a;
+ }
+ else {
+ // No, it wasn't variant B, let's see if we can find an AB test where
+ // the given mailing was the winner (C).
$mailing_id_a = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingAB', $mailing_id, 'mailing_id_a', 'mailing_id_c');
- $field = 'mailing_id_c';
+ if (!empty($mailing_id_a)) {
+ // OK, this was the winner and we looked up variant A which is the relevant one.
+ $relevant_mailing_id = $mailing_id_a;
+ }
+ // (otherwise we were passed in variant A so we already have the relevant_mailing_id correct already.)
}
- $jobJoin = "INNER JOIN $ab ON $ab.mailing_id_a = $mg.mailing_id
- INNER JOIN $job ON $job.mailing_id = $ab.$field";
- $entity = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_MailingGroup', $mailing_id_a, 'entity_table', 'mailing_id');
- }
- else {
- $jobJoin = "INNER JOIN $job ON $job.mailing_id = $mg.mailing_id";
}
- $groupClause = '';
- if ($entity == $group) {
- $groupClause = "AND $group.is_hidden = 0";
- }
-
- $do->query("
- SELECT $mg.entity_table as entity_table,
- $mg.entity_id as entity_id,
- $mg.group_type as group_type
- FROM $mg
- $jobJoin
- INNER JOIN $entity
- ON $mg.entity_id = $entity.id
- WHERE $job.id = " . CRM_Utils_Type::escape($job_id, 'Integer') . "
- AND $mg.group_type IN ('Include', 'Base') $groupClause"
- );
-
- // Make a list of groups and a list of prior mailings that received
- // this mailing.
+ // Make a list of groups and a list of prior mailings that received this
+ // mailing. Nb. the 'Base' group is called the 'Unsubscribe group' in the
+ // UI.
+ // Just to definitely make it SQL safe.
+ $relevant_mailing_id = (int) $relevant_mailing_id;
+ $do = CRM_Core_DAO::executeQuery(
+ "SELECT entity_table, entity_id, group_type
+ FROM civicrm_mailing_group
+ WHERE mailing_id = $relevant_mailing_id
+ AND group_type IN ('Include', 'Base')");
$groups = [];
$base_groups = [];
$mailings = [];
while ($do->fetch()) {
- if ($do->entity_table == $group) {
+ if ($do->entity_table === $groupTableName) {
if ($do->group_type == 'Base') {
$base_groups[$do->entity_id] = NULL;
}
$groups[$do->entity_id] = NULL;
}
}
- elseif ($do->entity_table == $mailing) {
+ elseif ($do->entity_table === $mailingTableName) {
$mailings[] = $do->entity_id;
}
}
while (!empty($mailings)) {
$do = CRM_Core_DAO::executeQuery("
- SELECT $mg.entity_table as entity_table,
- $mg.entity_id as entity_id
- FROM civicrm_mailing_group $mg
- WHERE $mg.mailing_id IN (" . implode(', ', $mailings) . ")
- AND $mg.group_type = 'Include'");
+ SELECT entity_table as entity_table,
+ entity_id as entity_id
+ FROM civicrm_mailing_group
+ WHERE mailing_id IN (" . implode(', ', $mailings) . ")
+ AND group_type = 'Include'");
$mailings = [];
while ($do->fetch()) {
- if ($do->entity_table == $group) {
+ if ($do->entity_table === $groupTableName) {
$groups[$do->entity_id] = TRUE;
}
- elseif ($do->entity_table == $mailing) {
+ elseif ($do->entity_table === $mailing) {
$mailings[] = $do->entity_id;
}
}
// base groups from search based mailings.
$baseGroupClause = '';
if (!empty($baseGroupIds)) {
- $baseGroupClause = "OR $group.id IN(" . implode(', ', $baseGroupIds) . ")";
+ $baseGroupClause = "OR grp.id IN(" . implode(', ', $baseGroupIds) . ")";
}
$groupIdClause = '';
if ($groupIds || $baseGroupIds) {
- $groupIdClause = "AND $group.id IN (" . implode(', ', array_merge($groupIds, $baseGroupIds)) . ")";
+ $groupIdClause = "AND grp.id IN (" . implode(', ', array_merge($groupIds, $baseGroupIds)) . ")";
}
$do = CRM_Core_DAO::executeQuery("
- SELECT $group.id as group_id,
- $group.title as title,
- $group.description as description
- FROM civicrm_group $group
- LEFT JOIN civicrm_group_contact $gc
- ON $gc.group_id = $group.id
- WHERE $group.is_hidden = 0
+ SELECT grp.id as group_id,
+ grp.title as title,
+ grp.description as description
+ FROM civicrm_group grp
+ LEFT JOIN civicrm_group_contact gc
+ ON gc.group_id = grp.id
+ WHERE grp.is_hidden = 0
$groupIdClause
- AND ($group.saved_search_id is not null
- OR ($gc.contact_id = $contact_id
- AND $gc.status = 'Added')
+ AND (grp.saved_search_id is not null
+ OR (gc.contact_id = $contact_id
+ AND gc.status = 'Added')
$baseGroupClause
)");
<?php
/*
+--------------------------------------------------------------------+
- | CiviCRM version 5 |
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2020 |
- +--------------------------------------------------------------------+
- | This file is a part of CiviCRM. |
- | |
- | CiviCRM is free software; you can copy, modify, and distribute it |
- | under the terms of the GNU Affero General Public License |
- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
- | |
- | CiviCRM is distributed in the hope that it will be useful, but |
- | WITHOUT ANY WARRANTY; without even the implied warranty of |
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
- | See the GNU Affero General Public License for more details. |
+ | Copyright CiviCRM LLC. All rights reserved. |
| |
- | You should have received a copy of the GNU Affero General Public |
- | License and the CiviCRM Licensing Exception along |
- | with this program; if not, contact CiviCRM LLC |
- | at info[AT]civicrm[DOT]org. If you have questions about the |
- | GNU Affero General Public License or the licensing of CiviCRM, |
- | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
* @package CiviCRM_APIv3
* @subpackage API_Job
*
- * @copyright CiviCRM LLC (c) 2004-2020
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
* @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
*
*/
}
public function tearDown() {
+ global $dbLocale;
+ if ($dbLocale) {
+ CRM_Core_I18n_Schema::makeSinglelingual('en_US');
+ }
parent::tearDown();
$this->assertNotEmpty($this->counts['hook_alterMailParams']);
}
], 1);
}
+ /**
+ * Data provider for testGitLabIssue1108
+ *
+ * First we run it without multiLingual mode, then with.
+ *
+ * This is because we test table names, which may have been translated in a
+ * multiLingual context.
+ *
+ */
+ public function multiLingual() {
+ return [[0], [1]];
+ }
+
+ /**
+ * - unsubscribe used dodgy SQL that only checked half of the polymorphic
+ * relationship in mailing_group, meaning it could match 'mailing 123'
+ * against _group_ 123.
+ *
+ * - also, an INNER JOIN on the group table hid the mailing-based
+ * mailing_group records.
+ *
+ * - in turn this inner join meant the query returned nothing, which then
+ * caused the code that is supposed to find the contact within those groups
+ * to basically find all the groups that the contact in or were smart groups.
+ *
+ * - in certain situations (which I have not been able to replicate in this
+ * test) it caused the unsubscribe to fail to find *any* groups to unsubscribe
+ * people from, thereby breaking the unsubscribe.
+ *
+ * @dataProvider multiLingual
+ *
+ */
+ public function testGitLabIssue1108($isMultiLingual) {
+
+ // We need to make sure the mailing IDs are higher than the groupIDs.
+ // We do this by adding mailings until the mailing.id value is at least 10
+ // higher than the highest group.id
+ // Note that creating a row in a transaction then rolling back the
+ // transaction still increments the AUTO_INCREMENT counter for the table.
+ // (If this behaviour ever changes we throw an exception.)
+ if ($isMultiLingual) {
+ $this->enableMultilingual();
+ }
+ $max_group_id = CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_group");
+ $max_mailing_id = 0;
+ while ($max_mailing_id < $max_group_id + 10) {
+ CRM_Core_Transaction::create()->run(function($tx) use (&$max_mailing_id) {
+ CRM_Core_DAO::executeQuery("INSERT INTO civicrm_mailing (name) VALUES ('dummy');");
+ $_ = (int) CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_mailing");
+ if ($_ === $max_mailing_id) {
+ throw new RuntimeException("Expected that creating a new row would increment ID, but it did not. This could be a change in MySQL's implementation of rollback");
+ }
+ $max_mailing_id = $_;
+ $tx->rollback();
+ });
+ }
+
+ // Because our parent class marks the _groupID as private, we can't use that :-(
+ $group_1 = $this->groupCreate([
+ 'name' => 'Test Group 1108.1',
+ 'title' => 'Test Group 1108.1',
+ ]);
+ $this->createContactsInGroup(2, $group_1);
+
+ // Also _mut is private to the parent, so we have to make our own:
+ $mut = new CiviMailUtils($this, TRUE);
+
+ // Create initial mailing to the group.
+ $mailingParams = [
+ 'name' => 'Issue 1108: mailing 1',
+ 'subject' => 'Issue 1108: mailing 1',
+ 'created_id' => 1,
+ 'groups' => ['include' => [$group_1]],
+ 'scheduled_date' => 'now',
+ 'body_text' => 'Please just {action.unsubscribe}',
+ ];
+
+ // The following code is exactly the same as runMailingSuccess() except that we store the ID of the mailing.
+ $mailing_1 = $this->callAPISuccess('mailing', 'create', $mailingParams);
+ $mut->assertRecipients(array());
+ $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
+
+ $allMessages = $mut->getAllMessages('ezc');
+ // There are exactly two contacts produced by setUp().
+ $this->assertEquals(2, count($allMessages));
+
+ // We need a new group
+ $group_2 = $this->groupCreate([
+ 'name' => 'Test Group 1108.2',
+ 'title' => 'Test Group 1108.2',
+ ]);
+
+ // Now create the 2nd mailing to the recipients of the first,
+ // excluding our new albeit empty group.
+ $mailingParams = [
+ 'name' => 'Issue 1108: mailing 2',
+ 'subject' => 'Issue 1108: mailing 2',
+ 'created_id' => 1,
+ 'mailings' => ['include' => [$mailing_1['id']]],
+ 'groups' => ['exclude' => [$group_2]],
+ 'scheduled_date' => 'now',
+ 'body_text' => 'Please just {action.unsubscribeUrl}',
+ ];
+ $this->callAPISuccess('mailing', 'create', $mailingParams);
+ $_ = $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
+
+ $allMessages = $mut->getAllMessages('ezc');
+ // We should have 2+2 messages sent by the mail system now.
+ $this->assertEquals(4, count($allMessages));
+
+ // So far so good.
+ // Now extract the unsubscribe details.
+ $message = end($allMessages);
+ $this->assertTrue($message->body instanceof ezcMailText);
+ $this->assertEquals('plain', $message->body->subType);
+ $this->assertEquals(1, preg_match(
+ '@mailing/unsubscribe.*jid=(\d+)&qid=(\d+)&h=([0-9a-z]+)@',
+ $message->body->text,
+ $matches
+ ));
+
+ // Create a group that has nothing to do with this mailing.
+ $group_3 = $this->groupCreate([
+ 'name' => 'Test Group 1108.3',
+ 'title' => 'Test Group 1108.3',
+ ]);
+ // Add contacts from group 1 to group 3.
+ $gcQuery = new CRM_Contact_BAO_GroupContact();
+ $gcQuery->group_id = $group_1;
+ $gcQuery->status = 'Added';
+ $gcQuery->find();
+ while ($gcQuery->fetch()) {
+ $this->callAPISuccess('group_contact', 'create',
+ ['group_id' => $group_3, 'contact_id' => $gcQuery->contact_id, 'status' => 'Added']);
+ }
+
+ // Part of the issue is caused by the fact that (at time of writing) the
+ // SQL joined the mailing_group table on just the entity_id, assuming it to
+ // be a group, but actually it could be a mailing.
+ // The difficulty in testing this is that because all our IDs are very low
+ // and contiguous the SQL looking for a match for 'mailing 1' does match a
+ // group ID of '1', which is created in this class's parent's setUp().
+ // Strictly speaking we don't know that it has ID 1, but as we can't access _groupID
+ // we'll have to assume that.
+ //
+ // So by deleting that group the SQL then matches nothing which is what we
+ // need for this case.
+ $_ = new CRM_Contact_BAO_Group();
+ $_->id = 1;
+ $_->delete();
+
+ $hooks = \CRM_Utils_Hook::singleton();
+ $found = [];
+ $hooks->setHook('civicrm_unsubscribeGroups',
+ function ($op, $mailingId, $contactId, &$groups, &$baseGroups) use (&$found) {
+ $found['groups'] = $groups;
+ $found['baseGroups'] = $baseGroups;
+ });
+
+ // Now test unsubscribe groups.
+ $groups = CRM_Mailing_Event_BAO_Unsubscribe::unsub_from_mailing(
+ $matches[1],
+ $matches[2],
+ $matches[3],
+ TRUE
+ );
+
+ // We expect that our group_1 was found.
+ $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
+
+ // We *should* get an array with just our $group_1 since this is the only group
+ // that we have included.
+ // $group_2 was only used to exclude people.
+ // $group_3 has nothing to do with this mailing and should not be there.
+ $this->assertNotEmpty($groups, "We should have received an array.");
+ $this->assertEquals([$group_1], array_keys($groups),
+ "We should have received an array with our group 1 in it.");
+ }
+
}