Merge pull request #21085 from totten/master-action-lang
authorTim Otten <totten@civicrm.org>
Wed, 11 Aug 2021 01:56:35 +0000 (18:56 -0700)
committerGitHub <noreply@github.com>
Wed, 11 Aug 2021 01:56:35 +0000 (18:56 -0700)
Scheduled Reminders - Pass locale through to TokenProcessor

CRM/Contact/Form/Task/EmailCommon.php
CRM/Contact/Form/Task/EmailTrait.php
CRM/Contribute/BAO/Contribution.php
CRM/Core/BAO/Email.php
CRM/Core/BAO/MessageTemplate.php
CRM/Upgrade/Incremental/General.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Utils/CoreUtil.php
templates/CRM/Contact/Form/Task/Email.tpl
tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php
tests/phpunit/api/v4/Action/FkJoinTest.php

index 8244a9440b1b22bac7591b1cc8e43bc055a3001b..07b0e8247de2d3cb1b469073eb255298a9f80f7d 100644 (file)
@@ -28,7 +28,7 @@ class CRM_Contact_Form_Task_EmailCommon {
    * @param CRM_Core_Form $form
    * @param bool $bounce determine if we want to throw a status bounce.
    *
-   * @throws \CiviCRM_API3_Exception
+   * @throws \API_Exception
    */
   public static function preProcessFromAddress(&$form, $bounce = TRUE) {
     $form->_emails = [];
@@ -49,20 +49,13 @@ class CRM_Contact_Form_Task_EmailCommon {
     $form->_emails = $fromEmailValues;
     $defaults = [];
     $form->_fromEmails = $fromEmailValues;
+    if (is_numeric(key($form->_fromEmails))) {
+      $emailID = (int) key($form->_fromEmails);
+      $defaults = CRM_Core_BAO_Email::getEmailSignatureDefaults($emailID);
+    }
     if (!Civi::settings()->get('allow_mail_from_logged_in_contact')) {
       $defaults['from_email_address'] = current(CRM_Core_BAO_Domain::getNameAndEmail(FALSE, TRUE));
     }
-    if (is_numeric(key($form->_fromEmails))) {
-      // Add signature
-      $defaultEmail = civicrm_api3('email', 'getsingle', ['id' => key($form->_fromEmails)]);
-      $defaults = [];
-      if (!empty($defaultEmail['signature_html'])) {
-        $defaults['html_message'] = '<br/><br/>--' . $defaultEmail['signature_html'];
-      }
-      if (!empty($defaultEmail['signature_text'])) {
-        $defaults['text_message'] = "\n\n--\n" . $defaultEmail['signature_text'];
-      }
-    }
     $form->setDefaults($defaults);
   }
 
index 31ec4f80315c3631239ef494b4a54c3a061881cb..d87cea587ceaab3ed2ae62ae5d35dd794ac46499 100644 (file)
@@ -118,10 +118,11 @@ trait CRM_Contact_Form_Task_EmailTrait {
    * Call trait preProcess function.
    *
    * This function exists as a transitional arrangement so classes overriding
-   * preProcess can still call it. Ideally it will be melded into preProcess later.
+   * preProcess can still call it. Ideally it will be melded into preProcess
+   * later.
    *
-   * @throws \CiviCRM_API3_Exception
    * @throws \CRM_Core_Exception
+   * @throws \API_Exception
    */
   protected function traitPreProcess() {
     CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this);
index 3093761de99aa776e6196a01f65bcf7749a9e631..0018c801ba1205c658604906dcbc4a97d1f167d6 100644 (file)
@@ -2090,9 +2090,6 @@ LEFT JOIN  civicrm_contribution contribution ON ( componentPayment.contribution_
     $contributionId = $params['contribution_id'];
     $contributionStatusId = $params['contribution_status_id'];
 
-    // if we already processed contribution object pass previous status id.
-    $previousContriStatusId = $params['previous_contribution_status_id'];
-
     // we process only ( Completed, Cancelled, or Failed ) contributions.
     if (!$contributionId || $contributionStatus !== 'Completed') {
       return;
@@ -2169,24 +2166,10 @@ LEFT JOIN  civicrm_contribution contribution ON ( componentPayment.contribution_
     if ($contributionStatus === 'Completed') {
 
       // only pending contribution related object processed.
-      if ($previousContriStatusId &&
-        !in_array($previousStatus, [
-          'Pending',
-          'Partially paid',
-        ])
-      ) {
+      if (!in_array($previousStatus, ['Pending', 'Partially paid'])) {
         // this is case when we already processed contribution object.
         return;
       }
-      elseif (!$previousContriStatusId &&
-        !in_array($contributionStatus, [
-          'Pending',
-          'Partially paid',
-        ])
-      ) {
-        // this is case when we are going to process contribution object later.
-        return;
-      }
 
       if (is_array($memberships)) {
         foreach ($memberships as $membership) {
index 1e947294a574da55bb1c5ccde522d23e6bdfb643..5341997ecb8d50fe7d37d80e30090029e0540adc 100644 (file)
@@ -15,6 +15,8 @@
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
 
+use Civi\Api4\Email;
+
 /**
  * This class contains functions for email handling.
  */
@@ -383,4 +385,25 @@ AND    reset_date IS NULL
     }
   }
 
+  /**
+   * Get default text for a message with the signature from the email sender populated.
+   *
+   * @param int $emailID
+   *
+   * @return array
+   *
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public static function getEmailSignatureDefaults(int $emailID): array {
+    // Add signature
+    $defaultEmail = Email::get(FALSE)
+      ->addSelect('signature_html', 'signature_text')
+      ->addWhere('id', '=', $emailID)->execute()->first();
+    return [
+      'html_message' => empty($defaultEmail['signature_html']) ? '' : '<br/><br/>--' . $defaultEmail['signature_html'],
+      'text_message' => empty($defaultEmail['signature_text']) ? '' : "\n\n--\n" . $defaultEmail['signature_text'],
+    ];
+  }
+
 }
index 2bd753b70d73fd7b8193554e26a26971dac119bc..0025b80e5472d5cdaa2119b16596a302dd00743d 100644 (file)
@@ -381,10 +381,17 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
       'valueName' => NULL,
       // ID of the template
       'messageTemplateID' => NULL,
+      // content of the message template
+      // Ex: ['msg_subject' => 'Hello {contact.display_name}', 'msg_html' => '...', 'msg_text' => '...']
+      // INTERNAL: 'messageTemplate' is currently only intended for use within civicrm-core only. For downstream usage, future updates will provide comparable public APIs.
+      'messageTemplate' => NULL,
       // contact id if contact tokens are to be replaced
       'contactId' => NULL,
       // additional template params (other than the ones already set in the template singleton)
       'tplParams' => [],
+      // additional token params (passed to the TokenProcessor)
+      // INTERNAL: 'tokenContext' is currently only intended for use within civicrm-core only. For downstream usage, future updates will provide comparable public APIs.
+      'tokenContext' => [],
       // the From: header
       'from' => NULL,
       // the recipient’s name
@@ -418,14 +425,9 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
       CRM_Core_Error::deprecatedWarning('message template id should be an integer');
       $params['messageTemplateID'] = (int) $params['messageTemplateID'];
     }
-    $mailContent = self::loadTemplate((string) $params['valueName'], $params['isTest'], $params['messageTemplateID'] ?? NULL, $params['groupName'] ?? '');
+    $mailContent = self::loadTemplate((string) $params['valueName'], $params['isTest'], $params['messageTemplateID'] ?? NULL, $params['groupName'] ?? '', $params['messageTemplate'], $params['subject'] ?? NULL);
 
-    // Overwrite subject from form field
-    if (!empty($params['subject'])) {
-      $mailContent['subject'] = $params['subject'];
-    }
-
-    $mailContent = self::renderMessageTemplate($mailContent, (bool) $params['disableSmarty'], $params['contactId'] ?? NULL, $params['tplParams']);
+    $mailContent = self::renderMessageTemplate($mailContent, (bool) $params['disableSmarty'], $params['contactId'] ?? NULL, $params['tplParams'], $params['tokenContext']);
 
     // send the template, honouring the target user’s preferences (if any)
     $sent = FALSE;
@@ -502,12 +504,18 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
    * @param bool $isTest
    * @param int|null $messageTemplateID
    * @param string $groupName
+   * @param array|null $messageTemplateOverride
+   *   Optionally, record with msg_subject, msg_text, msg_html.
+   *   If omitted, the record will be loaded from workflowName/messageTemplateID.
+   * @param string|null $subjectOverride
+   *   This option is the older, wonkier version of $messageTemplate['msg_subject']...
    *
    * @return array
    * @throws \API_Exception
    * @throws \CRM_Core_Exception
    */
-  protected static function loadTemplate(string $workflowName, bool $isTest, int $messageTemplateID = NULL, $groupName = NULL): array {
+  protected static function loadTemplate(string $workflowName, bool $isTest, int $messageTemplateID = NULL, $groupName = NULL, ?array $messageTemplateOverride = NULL, ?string $subjectOverride = NULL): array {
+    $base = ['msg_subject' => NULL, 'msg_text' => NULL, 'msg_html' => NULL, 'pdf_format_id' => NULL];
     if (!$workflowName && !$messageTemplateID) {
       throw new CRM_Core_Exception(ts("Message template's option value or ID missing."));
     }
@@ -522,12 +530,12 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
     else {
       $apiCall->addWhere('workflow_name', '=', $workflowName);
     }
-    $messageTemplate = $apiCall->execute()->first();
-    if (empty($messageTemplate['id'])) {
+    $messageTemplate = array_merge($base, $apiCall->execute()->first() ?: [], $messageTemplateOverride ?: []);
+    if (empty($messageTemplate['id']) && empty($messageTemplateOverride)) {
       if ($messageTemplateID) {
         throw new CRM_Core_Exception(ts('No such message template: id=%1.', [1 => $messageTemplateID]));
       }
-      throw new CRM_Core_Exception(ts('No message template with workflow name %2.', [2 => $workflowName]));
+      throw new CRM_Core_Exception(ts('No message template with workflow name %1.', [1 => $workflowName]));
     }
 
     $mailContent = [
@@ -564,6 +572,11 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
       $mailContent['html'] = preg_replace('/<body(.*)$/im', "<body\\1\n{$testText['msg_html']}", $mailContent['html']);
     }
 
+    if (!empty($subjectOverride)) {
+      CRM_Core_Error::deprecatedWarning('CRM_Core_BAO_MessageTemplate: $params[subject] is deprecated. Use $params[messageTemplate][msg_subject] instead.');
+      $mailContent['subject'] = $subjectOverride;
+    }
+
     return $mailContent;
   }
 
@@ -577,12 +590,15 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
    * @param bool $disableSmarty
    * @param int|NULL $contactID
    * @param array $smartyAssigns
+   *   Data to pass through to Smarty.
+   * @param array $tokenContext
+   *   Data to pass through to TokenProcessor.
    *
    * @return array
    */
-  public static function renderMessageTemplate(array $mailContent, bool $disableSmarty, $contactID, array $smartyAssigns): array {
-    $tokenContext = ['smarty' => !$disableSmarty];
-    if ($contactID) {
+  public static function renderMessageTemplate(array $mailContent, bool $disableSmarty, $contactID, array $smartyAssigns, array $tokenContext = []): array {
+    $tokenContext['smarty'] = !$disableSmarty;
+    if ($contactID && !isset($tokenContext['contactId'])) {
       $tokenContext['contactId'] = $contactID;
     }
     $result = CRM_Core_TokenSmarty::render(CRM_Utils_Array::subset($mailContent, ['text', 'html', 'subject']), $tokenContext, $smartyAssigns);
index 115e47a7d01e2d7fa04a5e38775f65a96f9610b1..9023a670c80090d355d5ee38d8ad92f16ca4a3a4 100644 (file)
@@ -122,8 +122,8 @@ class CRM_Upgrade_Incremental_General {
     }
 
     $ftAclSetting = Civi::settings()->get('acl_financial_type');
-    $financialAclExtension = civicrm_api3('extension', 'get', ['key' => 'biz.jmaconsulting.financialaclreport']);
-    if ($ftAclSetting && (($financialAclExtension['count'] == 1 && $financialAclExtension['status'] != 'Installed') || $financialAclExtension['count'] !== 1)) {
+    $financialAclExtension = civicrm_api3('extension', 'get', ['key' => 'biz.jmaconsulting.financialaclreport', 'sequential' => 1]);
+    if ($ftAclSetting && (($financialAclExtension['count'] == 1 && $financialAclExtension['values'][0]['status'] != 'Installed') || $financialAclExtension['count'] !== 1)) {
       $preUpgradeMessage .= '<br />' . ts('CiviCRM will in the future require the extension %1 for CiviCRM Reports to work correctly with the Financial Type ACLs. The extension can be downloaded <a href="%2">here</a>', [
         1 => 'biz.jmaconsulting.financialaclreport',
         2 => 'https://github.com/JMAConsulting/biz.jmaconsulting.financialaclreport',
index 7232d02b3f978c973dc2ad9826a62f3f60aed1e9..c3e6e3c09af78d86123d65413f881b3a1bff1e5c 100644 (file)
@@ -680,7 +680,10 @@ class Api4SelectQuery {
         continue;
       }
       // Ensure alias is a safe string, and supply default if not given
-      $alias = $alias ? \CRM_Utils_String::munge($alias, '_', 256) : strtolower($entity);
+      $alias = $alias ?: strtolower($entity);
+      if ($alias === self::MAIN_TABLE_ALIAS || !preg_match('/^[-\w]{1,256}$/', $alias)) {
+        throw new \API_Exception('Illegal join alias: "' . $alias . '"');
+      }
       // First item in the array is a boolean indicating if the join is required (aka INNER or LEFT).
       // The rest are join conditions.
       $side = array_shift($join);
index 7d27b9ea247f5e6a34317dea9e9ccfb50bf5278a..0c9f9612a6558d263f69b9b4f1ccb76e5f78b075 100644 (file)
@@ -60,7 +60,8 @@ class CoreUtil {
    * @return mixed
    */
   public static function getInfoItem(string $entityName, string $keyToReturn) {
-    return self::getApiClass($entityName)::getInfo()[$keyToReturn] ?? NULL;
+    $className = self::getApiClass($entityName);
+    return $className ? $className::getInfo()[$keyToReturn] ?? NULL : NULL;
   }
 
   /**
index b4d143ca67173e4474b6f97ed9bd895cffede965..9e21ef1faea624324f06ed41c113266da56fc6a9 100644 (file)
@@ -124,8 +124,7 @@ CRM.$(function($) {
   }
 
   {/literal}
-  var toContact = {if $toContact}{$toContact}{else}''{/if},
-    ccContact = {if $ccContact}{$ccContact}{else}''{/if};
+  var toContact = {if $toContact}{$toContact}{else}''{/if};
   {literal}
   emailSelect('#to', toContact);
 });
index fa5003a2ea92c447bc6ad714c5119417b90c04b0..08edc85ca5a80d117b0fde312f826e92091715a5 100644 (file)
@@ -22,6 +22,111 @@ class CRM_Core_BAO_MessageTemplateTest extends CiviUnitTestCase {
     parent::tearDown();
   }
 
+  public function testSendTemplate_RenderMode_OpenTemplate() {
+    $contactId = $this->individualCreate([
+      'first_name' => 'Abba',
+      'last_name' => 'Baab',
+      'prefix_id' => NULL,
+      'suffix_id' => NULL,
+    ]);
+    [$sent, $subject, $messageText, $messageHtml] = CRM_Core_BAO_MessageTemplate::sendTemplate(
+      [
+        'valueName' => 'case_activity',
+        'contactId' => $contactId,
+        'from' => 'admin@example.com',
+        // No 'toEmail'/'toName' address => not sendable, but still returns rendered value.
+        'attachments' => NULL,
+        'messageTemplate' => [
+          'msg_subject' => 'Hello testSendTemplate_RenderMode_OpenTemplate {contact.display_name}!',
+          'msg_text' => 'Hello testSendTemplate_RenderMode_OpenTemplate {contact.display_name}!',
+          'msg_html' => '<p>Hello testSendTemplate_RenderMode_OpenTemplate {contact.display_name}!</p>',
+        ],
+      ]
+    );
+    $this->assertEquals(FALSE, $sent);
+    $this->assertEquals('Hello testSendTemplate_RenderMode_OpenTemplate Abba Baab!', $subject);
+    $this->assertEquals('Hello testSendTemplate_RenderMode_OpenTemplate Abba Baab!', $messageText);
+    $this->assertStringContainsString('<p>Hello testSendTemplate_RenderMode_OpenTemplate Abba Baab!</p>', $messageHtml);
+  }
+
+  public function testSendTemplate_RenderMode_DefaultTpl() {
+    CRM_Core_Transaction::create(TRUE)->run(function(CRM_Core_Transaction $tx) {
+      $tx->rollback();
+
+      \Civi\Api4\MessageTemplate::update()
+        ->addWhere('workflow_name', '=', 'case_activity')
+        ->addWhere('is_reserved', '=', 0)
+        ->setValues([
+          'msg_subject' => 'Hello testSendTemplate_RenderMode_Default {contact.display_name}!',
+          'msg_text' => 'Hello testSendTemplate_RenderMode_Default {contact.display_name}!',
+          'msg_html' => '<p>Hello testSendTemplate_RenderMode_Default {contact.display_name}!</p>',
+        ])
+        ->execute();
+
+      $contactId = $this->individualCreate([
+        'first_name' => 'Abba',
+        'last_name' => 'Baab',
+        'prefix_id' => NULL,
+        'suffix_id' => NULL,
+      ]);
+
+      [$sent, $subject, $messageText, $messageHtml] = CRM_Core_BAO_MessageTemplate::sendTemplate(
+        [
+          'valueName' => 'case_activity',
+          'contactId' => $contactId,
+          'from' => 'admin@example.com',
+          // No 'toEmail'/'toName' address => not sendable, but still returns rendered value.
+          'attachments' => NULL,
+        ]
+      );
+      $this->assertEquals(FALSE, $sent);
+      $this->assertEquals('Hello testSendTemplate_RenderMode_Default Abba Baab!', $subject);
+      $this->assertEquals('Hello testSendTemplate_RenderMode_Default Abba Baab!', $messageText);
+      $this->assertStringContainsString('<p>Hello testSendTemplate_RenderMode_Default Abba Baab!</p>', $messageHtml);
+    });
+  }
+
+  public function testSendTemplate_RenderMode_TokenContext() {
+    CRM_Core_Transaction::create(TRUE)->run(function(CRM_Core_Transaction $tx) {
+      $tx->rollback();
+
+      \Civi\Api4\MessageTemplate::update()
+        ->addWhere('workflow_name', '=', 'case_activity')
+        ->addWhere('is_reserved', '=', 0)
+        ->setValues([
+          'msg_subject' => 'Hello {contact.display_name} about {activity.subject}!',
+          'msg_text' => 'Hello {contact.display_name} about {activity.subject}!',
+          'msg_html' => '<p>Hello {contact.display_name} about {activity.subject}!</p>',
+        ])
+        ->execute();
+
+      $contactId = $this->individualCreate([
+        'first_name' => 'Abba',
+        'last_name' => 'Baab',
+        'prefix_id' => NULL,
+        'suffix_id' => NULL,
+      ]);
+      $activityId = $this->activityCreate(['subject' => 'Something Something'])['id'];
+
+      [$sent, $subject, $messageText, $messageHtml] = CRM_Core_BAO_MessageTemplate::sendTemplate(
+        [
+          'valueName' => 'case_activity',
+          'tokenContext' => [
+            'contactId' => $contactId,
+            'activityId' => $activityId,
+          ],
+          'from' => 'admin@example.com',
+          // No 'toEmail'/'toName' address => not sendable, but still returns rendered value.
+          'attachments' => NULL,
+        ]
+      );
+      $this->assertEquals(FALSE, $sent);
+      $this->assertEquals('Hello Abba Baab about Something Something!', $subject);
+      $this->assertEquals('Hello Abba Baab about Something Something!', $messageText);
+      $this->assertStringContainsString('<p>Hello Abba Baab about Something Something!</p>', $messageHtml);
+    });
+  }
+
   /**
    * Test message template send.
    *
index d0ed5c87b1b064d37bde1ff25ddc1689e4f2e22c..91aff6ea3b1b06bfc2c31c1daad9631313e6d4e0 100644 (file)
@@ -121,6 +121,40 @@ class FkJoinTest extends UnitTestCase {
     $this->assertNotContains($this->getReference('test_contact_1')['id'], $contacts);
   }
 
+  public function testInvalidJoinAlias() {
+    // Not allowed to use same alias as the base table
+    try {
+      Contact::get(FALSE)->addJoin('Address AS a')->execute();
+    }
+    catch (\API_Exception $e) {
+      $message = $e->getMessage();
+    }
+    $this->assertEquals('Illegal join alias: "a"', $message);
+
+    // Not allowed to use dots in the alias
+    try {
+      Contact::get(FALSE)->addJoin('Address AS add.ress')->execute();
+    }
+    catch (\API_Exception $e) {
+      $message = $e->getMessage();
+    }
+    $this->assertEquals('Illegal join alias: "add.ress"', $message);
+
+    // Not allowed to use an alias > 256 characters
+    try {
+      $longAlias = str_repeat('z', 257);
+      Contact::get(FALSE)->addJoin("Address AS $longAlias")->execute();
+    }
+    catch (\API_Exception $e) {
+      $message = $e->getMessage();
+    }
+    $this->assertEquals("Illegal join alias: \"$longAlias\"", $message);
+
+    // Alpha-numeric with dashes 256 characters long - weird but allowed
+    $okAlias = str_repeat('-0_a-9Z_', 32);
+    Contact::get(FALSE)->addJoin("Address AS $okAlias")->execute();
+  }
+
   public function testJoinToTheSameTableTwice() {
     $cid1 = Contact::create(FALSE)
       ->addValue('first_name', 'Aaa')