dev/core#2814 Fix tokenCompatSubscriber to conditionally evaluate legacy hook tokens
authorEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 13 Sep 2021 01:45:38 +0000 (13:45 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 14 Sep 2021 05:12:07 +0000 (17:12 +1200)
Civi/Token/TokenCompatSubscriber.php
tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php

index 417450ef93b3f7f19bf3e84f3c52c05ff4ef0b83..79eac1f25955a9cfa71ad3a8d292cf2823c1e6d3 100644 (file)
@@ -30,6 +30,7 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     return [
       'civi.token.eval' => [
         ['setupSmartyAliases', 1000],
+        ['evaluateLegacyHookTokens', 500],
         ['onEvaluate'],
       ],
       'civi.token.render' => 'onRender',
@@ -242,6 +243,57 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     $proc->addMessage('TokenCompatSubscriber.aliases', $fakeMessage, 'text/plain');
   }
 
+  /**
+   * Load token data from legacy hooks.
+   *
+   * While our goal is for people to move towards implementing
+   * toke processors the old-style hooks can extend contact
+   * token data.
+   *
+   * When that is happening we need to load the full contact record
+   * to send to the hooks (not great for performance but the
+   * fix is to move away from implementing legacy style hooks).
+   *
+   * Consistent with prior behaviour we only load the contact it it
+   * is already loaded. In that scenario we also load any extra fields
+   * that might be wanted for the contact tokens.
+   *
+   * @param \Civi\Token\Event\TokenValueEvent $e
+   * @throws TokenException
+   */
+  public function evaluateLegacyHookTokens(TokenValueEvent $e): void {
+    $messageTokens = $e->getTokenProcessor()->getMessageTokens();
+    $hookTokens = array_intersect(\CRM_Utils_Token::getTokenCategories(), array_keys($messageTokens));
+    if (empty($hookTokens)) {
+      return;
+    }
+    foreach ($e->getRows() as $row) {
+      if (empty($row->context['contactId'])) {
+        continue;
+      }
+      unset($swapLocale);
+      $swapLocale = empty($row->context['locale']) ? NULL : \CRM_Utils_AutoClean::swapLocale($row->context['locale']);
+      if (empty($row->context['contact'])) {
+        // If we don't have the contact already load it now, getting full
+        // details for hooks and anything the contact token resolution might
+        // want later.
+        $row->context['contact'] = $this->getContact($row->context['contactId'], $messageTokens['contact'] ?? [], TRUE);
+      }
+      $contactArray = [$row->context['contactId'] => $row->context['contact']];
+      \CRM_Utils_Hook::tokenValues($contactArray,
+        [$row->context['contactId']],
+        empty($row->context['mailingJobId']) ? NULL : $row->context['mailingJobId'],
+        $messageTokens,
+        $row->context['controller']
+      );
+      foreach ($hookTokens as $hookToken) {
+        foreach ($messageTokens[$hookToken] as $tokenName) {
+          $row->format('text/plain')->tokens($hookToken, $tokenName, $contactArray[$row->context['contactId']][$tokenName] ?? '');
+        }
+      }
+    }
+  }
+
   /**
    * Load token data.
    *
@@ -255,7 +307,7 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
 
     $e->getTokenProcessor()->context['hookTokenCategories'] = \CRM_Utils_Token::getTokenCategories();
 
-    $messageTokens = $e->getTokenProcessor()->getMessageTokens();
+    $messageTokens = $e->getTokenProcessor()->getMessageTokens()['contact'] ?? [];
 
     foreach ($e->getRows() as $row) {
       if (empty($row->context['contactId'])) {
@@ -268,24 +320,11 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
       /** @var int $contactId */
       $contactId = $row->context['contactId'];
       if (empty($row->context['contact'])) {
-        $contact = $this->getContact($contactId, $messageTokens['contact'] ?? [], TRUE);
+        $contact = $this->getContact($contactId, $messageTokens);
       }
       else {
         $contact = $row->context['contact'];
       }
-
-      $contactArray = [$contactId => $contact];
-      \CRM_Utils_Hook::tokenValues($contactArray,
-        [$contactId],
-        empty($row->context['mailingJobId']) ? NULL : $row->context['mailingJobId'],
-        $messageTokens,
-        $row->context['controller']
-      );
-
-      // merge the custom tokens in the $contact array
-      if (!empty($contactArray[$contactId])) {
-        $contact = array_merge($contact, $contactArray[$contactId]);
-      }
       $row->context('contact', $contact);
     }
   }
index b288a7ca98eca0c92e8068b2573a0ef3528a439e..0f25e72d6b4f00a24254c06c5d05362e346a049d 100644 (file)
@@ -339,6 +339,7 @@ London, 90210
     // work in combination.
     $tokenString = '{$pre_assigned_smarty}{$passed_smarty}
 {domain.name}
+{important_stuff.favourite_emoticon}
 ';
     foreach (array_keys($tokenData) as $key) {
       $tokenString .= "{$key}:{contact.{$key}}\n";
@@ -356,6 +357,7 @@ London, 90210
     ]);
     $expected = 'weewhoa
 Default Domain Name
+emo
 ';
     $expected .= $this->getExpectedContactOutput($address['id'], $tokenData, $messageContent['html']);
     $this->assertEquals($expected, $messageContent['html']);
@@ -455,7 +457,12 @@ Default Domain Name
     ];
   }
 
-  public function getAdvertisedTokens() {
+  /**
+   * Get the tokens we expect to see advertised.
+   *
+   * @return string[]
+   */
+  public function getAdvertisedTokens(): array {
     return [
       '{contact.contact_type}' => 'Contact Type',
       '{contact.do_not_email}' => 'Do Not Email',