diff --git a/CRM/Mailing/ActionTokens.php b/CRM/Mailing/ActionTokens.php new file mode 100644 index 00000000000..7bdbc124d2a --- /dev/null +++ b/CRM/Mailing/ActionTokens.php @@ -0,0 +1,105 @@ + int, event queue ID + * 'hash' => string, event queue hash code + * 'contact_id' => int, contact_id, + * 'email' => string, email + * 'phone' => string, phone + */ +class CRM_Mailing_ActionTokens extends \Civi\Token\AbstractTokenSubscriber { + + /** + * Class constructor. + */ + public function __construct() { + // TODO: Think about supporting dynamic tokens like "{action.subscribe.\d+}" + parent::__construct('action', array( + 'subscribeUrl' => ts('Subscribe URL (Action)'), + 'forward' => ts('Forward URL (Action)'), + 'optOut' => ts('Opt-Out (Action)'), + 'optOutUrl' => ts('Opt-Out URL (Action)'), + 'reply' => ts('Reply (Action)'), + 'unsubscribe' => ts('Unsubscribe (Action)'), + 'unsubscribeUrl' => ts('Unsubscribe URL (Action)'), + 'resubscribe' => ts('Resubscribe (Action)'), + 'resubscribeUrl' => ts('Resubscribe URL (Action)'), + 'eventQueueId' => ts('Event Queue ID'), + )); + } + + /** + * @inheritDoc + */ + public function evaluateToken( + \Civi\Token\TokenRow $row, + $entity, + $field, + $prefetch = NULL + ) { + // Most CiviMail action tokens were implemented via getActionTokenReplacement(). + // However, {action.subscribeUrl} has a second implementation via + // replaceSubscribeInviteTokens(). The two appear mostly the same. + // We use getActionTokenReplacement() since it's more consistent. However, + // this doesn't provide the dynamic/parameterized tokens of + // replaceSubscribeInviteTokens(). + + if (empty($row->context['mailingJobId']) || empty($row->context['mailingActionTarget']['hash'])) { + throw new \CRM_Core_Exception("Error: Cannot use action tokens unless context defines mailingJobId and mailingActionTarget."); + } + + if ($field === 'eventQueueId') { + $row->format('text/plain')->tokens($entity, $field, $row->context['mailingActionTarget']['id']); + return; + } + + list($verp, $urls) = CRM_Mailing_BAO_Mailing::getVerpAndUrls( + $row->context['mailingJobId'], + $row->context['mailingActionTarget']['id'], + $row->context['mailingActionTarget']['hash'], + // Note: Behavior is already undefined for SMS/'phone' mailings... + $row->context['mailingActionTarget']['email'] + ); + + $row->format('text/plain')->tokens($entity, $field, + CRM_Utils_Token::getActionTokenReplacement( + $field, $verp, $urls, FALSE)); + $row->format('text/html')->tokens($entity, $field, + CRM_Utils_Token::getActionTokenReplacement( + $field, $verp, $urls, TRUE)); + } + +} diff --git a/CRM/Mailing/Tokens.php b/CRM/Mailing/Tokens.php new file mode 100644 index 00000000000..5fa086d8c26 --- /dev/null +++ b/CRM/Mailing/Tokens.php @@ -0,0 +1,86 @@ + ts('Mailing ID'), + 'name' => ts('Mailing Name'), + 'group' => ts('Mailing Group(s)'), + 'subject' => ts('Mailing Subject'), + 'viewUrl' => ts('Mailing URL (View)'), + 'editUrl' => ts('Mailing URL (Edit)'), + 'scheduleUrl' => ts('Mailing URL (Schedule)'), + 'html' => ts('Mailing HTML'), + 'approvalStatus' => ts('Mailing Approval Status'), + 'approvalNote' => ts('Mailing Approval Note'), + 'approveUrl' => ts('Mailing Approval URL'), + 'creator' => ts('Mailing Creator (Name)'), + 'creatorEmail' => ts('Mailing Creator (Email)'), + )); + } + + /** + * @inheritDoc + */ + public function checkActive(\Civi\Token\TokenProcessor $processor) { + return !empty($processor->context['mailingId']) || !empty($processor->context['mailing']); + } + + public function prefetch(\Civi\Token\Event\TokenValueEvent $e) { + $processor = $e->getTokenProcessor(); + $mailing = isset($processor->context['mailing']) + ? $processor->context['mailing'] + : CRM_Mailing_BAO_Mailing::findById($processor->context['mailingId']); + + return array( + 'mailing' => $mailing, + ); + } + + /** + * @inheritDoc + */ + public function evaluateToken(\Civi\Token\TokenRow $row, $entity, $field, $prefetch = NULL) { + $row->format('text/plain')->tokens($entity, $field, + (string) CRM_Utils_Token::getMailingTokenReplacement($field, $prefetch['mailing'])); + } + +} diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 4acec9fe67b..20733e29802 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -203,8 +203,12 @@ public function createContainer() { 'Civi\Token\TokenCompatSubscriber', array() ))->addTag('kernel.event_subscriber'); + $container->setDefinition("crm_mailing_action_tokens", new Definition( + "CRM_Mailing_ActionTokens", + array() + ))->addTag('kernel.event_subscriber'); - foreach (array('Activity', 'Contribute', 'Event', 'Member') as $comp) { + foreach (array('Activity', 'Contribute', 'Event', 'Mailing', 'Member') as $comp) { $container->setDefinition("crm_" . strtolower($comp) . "_tokens", new Definition( "CRM_{$comp}_Tokens", array() diff --git a/Civi/Token/AbstractTokenSubscriber.php b/Civi/Token/AbstractTokenSubscriber.php index a7d21f1344b..25137b6ac63 100644 --- a/Civi/Token/AbstractTokenSubscriber.php +++ b/Civi/Token/AbstractTokenSubscriber.php @@ -141,10 +141,21 @@ public function evaluateTokens(TokenValueEvent $e) { if (!$this->checkActive($e->getTokenProcessor())) { return; } - // TODO: check if any tokens for $entity are actually used; short-circuit. + + $messageTokens = $e->getTokenProcessor()->getMessageTokens(); + if (!isset($messageTokens[$this->entity])) { + return; + } + + $activeTokens = array_intersect($messageTokens[$this->entity], array_keys($this->tokenNames)); + if (empty($activeTokens)) { + return; + } + $prefetch = $this->prefetch($e); + foreach ($e->getRows() as $row) { - foreach ($this->tokenNames as $field => $label) { + foreach ($activeTokens as $field) { $this->evaluateToken($row, $this->entity, $field, $prefetch); } } diff --git a/tests/phpunit/CRM/Mailing/TokensTest.php b/tests/phpunit/CRM/Mailing/TokensTest.php new file mode 100644 index 00000000000..2cbd3124be7 --- /dev/null +++ b/tests/phpunit/CRM/Mailing/TokensTest.php @@ -0,0 +1,135 @@ +useTransaction(); + parent::setUp(); + $this->callAPISuccess('mail_settings', 'get', + array('api.mail_settings.create' => array('domain' => 'chaos.org'))); + } + + public function getExampleTokens() { + $cases = array(); + + $cases[] = array('text/plain', 'The {mailing.id}!', ';The [0-9]+!;'); + $cases[] = array('text/plain', 'The {mailing.name}!', ';The Example Name!;'); + $cases[] = array('text/plain', 'The {mailing.editUrl}!', ';The http.*civicrm/mailing/send.*!;'); + $cases[] = array('text/plain', 'To subscribe: {action.subscribeUrl}!', ';To subscribe: http.*civicrm/mailing/subscribe.*!;'); + $cases[] = array('text/plain', 'To optout: {action.optOutUrl}!', ';To optout: http.*civicrm/mailing/optout.*!;'); + $cases[] = array('text/plain', 'To unsubscribe: {action.unsubscribe}!', ';To unsubscribe: u\.123\.456\.abcd1234@chaos.org!;'); + + // TODO: Think about supporting dynamic tokens like "{action.subscribe.\d+}" + + return $cases; + } + + /** + * Check that mailing-tokens are generated (given a mailing_id as input). + * + * @param string $inputTemplateFormat + * Ex: 'text/plain' or 'text/html' + * @param string $inputTemplate + * Ex: 'Hello, {contact.first_name}'. + * @param string $expectRegex + * @dataProvider getExampleTokens + */ + public function testTokensWithMailingId($inputTemplateFormat, $inputTemplate, $expectRegex) { + $mailing = CRM_Core_DAO::createTestObject('CRM_Mailing_DAO_Mailing', array( + 'name' => 'Example Name', + )); + $contact = CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + + $p = new \Civi\Token\TokenProcessor(Civi::service('dispatcher'), array( + 'mailingId' => $mailing->id, + )); + $p->addMessage('example', $inputTemplate, $inputTemplateFormat); + $p->addRow()->context(array( + 'contactId' => $contact->id, + 'mailingJobId' => 123, + 'mailingActionTarget' => array( + 'id' => 456, + 'hash' => 'abcd1234', + 'email' => 'someone@example.com', + ), + )); + $p->evaluate(); + $count = 0; + foreach ($p->getRows() as $row) { + $this->assertRegExp($expectRegex, $row->render('example')); + $count++; + } + $this->assertEquals(1, $count); + } + + /** + * Check that mailing-tokens are generated (given a mailing DAO as input). + */ + public function testTokensWithMailingObject() { + // We only need one case to see that the mailing-object works as + // an alternative to the mailing-id. + $inputTemplateFormat = 'text/plain'; + $inputTemplate = 'To optout: {action.optOutUrl}!'; + $expectRegex = ';To optout: http.*civicrm/mailing/optout.*!;'; + + $mailing = CRM_Core_DAO::createTestObject('CRM_Mailing_DAO_Mailing', array( + 'name' => 'Example Name', + )); + $contact = CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + + $p = new \Civi\Token\TokenProcessor(Civi::service('dispatcher'), array( + 'mailing' => $mailing, + )); + $p->addMessage('example', $inputTemplate, $inputTemplateFormat); + $p->addRow()->context(array( + 'contactId' => $contact->id, + 'mailingJobId' => 123, + 'mailingActionTarget' => array( + 'id' => 456, + 'hash' => 'abcd1234', + 'email' => 'someone@example.com', + ), + )); + $p->evaluate(); + $count = 0; + foreach ($p->getRows() as $row) { + $this->assertRegExp($expectRegex, $row->render('example')); + $count++; + } + $this->assertEquals(1, $count); + } + + /** + * Check the behavior in the erroneous situation where someone uses + * a mailing-related token without providing a mailing ID. + */ + public function testTokensWithoutMailing() { + // We only need one case to see that the mailing-object works as + // an alternative to the mailing-id. + $inputTemplateFormat = 'text/plain'; + $inputTemplate = 'To optout: {action.optOutUrl}!'; + + $mailing = CRM_Core_DAO::createTestObject('CRM_Mailing_DAO_Mailing', array( + 'name' => 'Example Name', + )); + $contact = CRM_Core_DAO::createTestObject('CRM_Contact_DAO_Contact'); + + $p = new \Civi\Token\TokenProcessor(Civi::service('dispatcher'), array( + 'mailing' => $mailing, + )); + $p->addMessage('example', $inputTemplate, $inputTemplateFormat); + $p->addRow()->context(array( + 'contactId' => $contact->id, + )); + try { + $p->evaluate(); + $this->fail('TokenProcessor::evaluate() should have thrown an exception'); + } + catch (CRM_Core_Exception $e) { + $this->assertRegExp(';Cannot use action tokens unless context defines mailingJobId and mailingActionTarget;', $e->getMessage()); + } + } + +}