CRM-19690 - CRM_Mailing_Tokens - Add TokenProcessor support
authorTim Otten <totten@civicrm.org>
Wed, 30 Nov 2016 00:07:26 +0000 (16:07 -0800)
committerTim Otten <totten@civicrm.org>
Tue, 20 Dec 2016 01:11:47 +0000 (17:11 -0800)
This includes support for both `{mailing.*}` and `{action.*}` tokens used in
CiviMail.

CRM/Mailing/ActionTokens.php [new file with mode: 0644]
CRM/Mailing/Tokens.php [new file with mode: 0644]
Civi/Core/Container.php
tests/phpunit/CRM/Mailing/TokensTest.php [new file with mode: 0644]

diff --git a/CRM/Mailing/ActionTokens.php b/CRM/Mailing/ActionTokens.php
new file mode 100644 (file)
index 0000000..bf3277b
--- /dev/null
@@ -0,0 +1,121 @@
+<?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'),
+    ));
+  }
+
+  /**
+   * Evaluate the content of a single token.
+   *
+   * @param \Civi\Token\TokenRow $row
+   *   The record for which we want token values.
+   * @param string $entity
+   * @param string $field
+   *   The name of the token field.
+   * @param mixed $prefetch
+   *   Any data that was returned by the prefetch().
+   *
+   * @return mixed
+   * @throws \CRM_Core_Exception
+   */
+  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));
+  }
+
+  protected function getTrackOpenUrl(\Civi\Token\TokenRow $row) {
+    $config = CRM_Core_Config::singleton();
+    return $config->userFrameworkResourceURL . "extern/open.php?q=" . $row->context['mailingActionTarget']['id'];
+  }
+
+}
diff --git a/CRM/Mailing/Tokens.php b/CRM/Mailing/Tokens.php
new file mode 100644 (file)
index 0000000..257386a
--- /dev/null
@@ -0,0 +1,100 @@
+<?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)'),
+    ));
+  }
+
+  /**
+   * Check something about being active.
+   *
+   * @param \Civi\Token\TokenProcessor $processor
+   *
+   * @return bool
+   */
+  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,
+    );
+  }
+
+  /**
+   * Evaluate the content of a single token.
+   *
+   * @param \Civi\Token\TokenRow $row
+   *   The record for which we want token values.
+   * @param string $entity
+   * @param string $field
+   *   The name of the token field.
+   * @param mixed $prefetch
+   *   Any data that was returned by the prefetch().
+   *
+   * @return mixed
+   */
+  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']));
+  }
+
+}
index 4acec9fe67b8235298b02fd753df94afab2bbf37..20733e29802e962af84840e8af46447dcbbdd153 100644 (file)
@@ -203,8 +203,12 @@ class Container {
       '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/tests/phpunit/CRM/Mailing/TokensTest.php b/tests/phpunit/CRM/Mailing/TokensTest.php
new file mode 100644 (file)
index 0000000..5461e69
--- /dev/null
@@ -0,0 +1,104 @@
+<?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);
+  }
+
+}