Skip to content

Commit

Permalink
Merge pull request #9563 from totten/master-19690-tokens
Browse files Browse the repository at this point in the history
CRM-19690 - CRM_Mailing_Tokens - Add TokenProcessor support
  • Loading branch information
totten authored Dec 23, 2016
2 parents 0682aca + 0ba33ff commit 68b4ca5
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 3 deletions.
105 changes: 105 additions & 0 deletions CRM/Mailing/ActionTokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2016 |
+--------------------------------------------------------------------+
| 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. |
| |
| 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 |
+--------------------------------------------------------------------+
*/

/**
* Class CRM_Mailing_ActionTokens
*
* Generate "action.*" tokens for mailings.
*
* To activate these tokens, the TokenProcessor context must specify:
* "mailingJobId" (int)
* "mailingActionTarget" (array) with keys:
* 'id' => 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));
}

}
86 changes: 86 additions & 0 deletions CRM/Mailing/Tokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2016 |
+--------------------------------------------------------------------+
| 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. |
| |
| 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 |
+--------------------------------------------------------------------+
*/

/**
* Class CRM_Mailing_Tokens
*
* Generate "mailing.*" tokens.
*
* To activate these tokens, the TokenProcessor context must specify either
* "mailingId" (int) or "mailing" (CRM_Mailing_BAO_Mailing).
*/
class CRM_Mailing_Tokens extends \Civi\Token\AbstractTokenSubscriber {

/**
* Class constructor.
*/
public function __construct() {
parent::__construct('mailing', array(
'id' => 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']));
}

}
6 changes: 5 additions & 1 deletion Civi/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 13 additions & 2 deletions Civi/Token/AbstractTokenSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
135 changes: 135 additions & 0 deletions tests/phpunit/CRM/Mailing/TokensTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

/**
* @group headless
*/
class CRM_Mailing_TokensTest extends \CiviUnitTestCase {
protected function setUp() {
$this->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());
}
}

}

0 comments on commit 68b4ca5

Please sign in to comment.