3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
12 use Civi\Token\TokenProcessor
;
13 use Civi\Api4\LocBlock
;
16 use Civi\Api4\Address
;
19 * CRM_Utils_TokenConsistencyTest
21 * Class for ensuring tokens have internal consistency.
27 class CRM_Utils_TokenConsistencyTest
extends CiviUnitTestCase
{
29 use CRMTraits_Custom_CustomDataTrait
;
39 * Recurring contribution.
43 protected $contributionRecur;
48 public function tearDown(): void
{
49 $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
50 $this->quickCleanUpFinancialEntities();
52 // WORKAROUND: CRM_Event_Tokens copies `civicrm_event` data into metadata cache. That should probably change, but that's a different scope-of-work.
53 // `clear()` works around it. This should be removed if that's updated, but it will be safe either way.
54 Civi
::cache('metadata')->clear();
60 * Test that case tokens are consistently rendered.
62 * @throws \CiviCRM_API3_Exception
64 public function testCaseTokenConsistency(): void
{
65 $this->createLoggedInUser();
66 CRM_Core_BAO_ConfigSetting
::enableComponent('CiviCase');
67 $this->createCustomGroupWithFieldOfType(['extends' => 'Case']);
68 $tokens = CRM_Core_SelectValues
::caseTokens();
69 $this->assertEquals($this->getCaseTokens(), $tokens);
70 $caseID = $this->getCaseID();
71 $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
72 $tokenHtml = CRM_Utils_Token
::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
73 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
74 // Now do the same without passing in 'knownTokens'
75 $tokenHtml = CRM_Utils_Token
::replaceCaseTokens($caseID, $tokenString);
76 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
78 // And check our deprecated tokens still work.
79 $tokenHtml = CRM_Utils_Token
::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}');
80 $this->assertEquals('Housing Support Ongoing', $tokenHtml);
81 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
82 'controller' => __CLASS__
,
84 'schema' => ['caseId'],
86 $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
87 $tokenProcessor->addRow([
88 'caseId' => $this->getCaseID(),
90 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
92 $tokenProcessor->evaluate();
93 foreach ($tokenProcessor->getRows() as $row) {
94 $text = $row->render('html');
96 $this->assertEquals($this->getExpectedCaseTokenOutput(), $text);
100 * Get expected output from token parsing.
104 protected function getExpectedCaseTokenOutput(): string {
106 case.case_type_id:label :Housing Support
107 case.subject :Case Subject
108 case.start_date :July 23rd, 2021
109 case.end_date :July 26th, 2021
110 case.details :case details
111 case.status_id:label :Ongoing
112 case.is_deleted:label :No
113 case.created_date :' . CRM_Utils_Date
::customFormat($this->case['created_date']) . '
114 case.modified_date :' . CRM_Utils_Date
::customFormat($this->case['modified_date']) . '
122 protected function getContactID(): int {
123 if (!isset($this->ids
['Contact'][0])) {
124 $this->ids
['Contact'][0] = $this->individualCreate();
126 return $this->ids
['Contact'][0];
130 * Get the keys for the case tokens.
134 public function getCaseTokenKeys(): array {
136 foreach (array_keys($this->getCaseTokens()) as $key) {
137 $return[] = substr($key, 6, -1);
143 * Get declared tokens.
147 public function getCaseTokens(): array {
149 '{case.id}' => 'Case ID',
150 '{case.case_type_id:label}' => 'Case Type',
151 '{case.subject}' => 'Case Subject',
152 '{case.start_date}' => 'Case Start Date',
153 '{case.end_date}' => 'Case End Date',
154 '{case.details}' => 'Details',
155 '{case.status_id:label}' => 'Case Status',
156 '{case.is_deleted:label}' => 'Case is in the Trash',
157 '{case.created_date}' => 'Created Date',
158 '{case.modified_date}' => 'Modified Date',
159 '{case.custom_1}' => 'Enter text here :: Group with field text',
168 protected function getCaseID(): int {
169 if (!isset($this->case)) {
170 $case_id = $this->callAPISuccess('Case', 'create', [
171 'case_type_id' => 'housing_support',
172 'activity_subject' => 'Case Subject',
173 'client_id' => $this->getContactID(),
175 'subject' => 'Case Subject',
176 'start_date' => '2021-07-23 15:39:20',
177 // Note end_date is inconsistent with status Ongoing but for the
178 // purposes of testing tokens is ok. Creating it with status Resolved
179 // then ignores our known fixed end date.
180 'end_date' => '2021-07-26 18:07:20',
182 'details' => 'case details',
183 'activity_details' => 'blah blah',
186 // Need to retrieve the case again because modified date might be updated a
187 // split-second later than the original return value because of activity
188 // triggers when the timeline is populated. The returned array from create
189 // is determined before that happens.
190 $this->case = $this->callAPISuccess('Case', 'getsingle', ['id' => $case_id]);
192 return $this->case['id'];
196 * Test that contribution recur tokens are consistently rendered.
198 public function testContributionRecurTokenConsistency(): void
{
199 $this->createLoggedInUser();
200 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
201 'controller' => __CLASS__
,
203 'schema' => ['contribution_recurId'],
205 $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens());
206 $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens());
207 $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
209 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
210 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
211 $tokenProcessor->evaluate();
212 $this->assertEquals($this->getExpectedContributionRecurTokenOutPut(), $tokenProcessor->getRow(0)->render('html'));
216 * Test that contribution recur tokens are consistently rendered.
218 public function testContributionRecurTokenRaw(): void
{
219 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
220 'controller' => __CLASS__
,
222 'schema' => ['contribution_recurId'],
224 $tokenProcessor->addMessage('not_specified', '{contribution_recur.amount}', 'text/plain');
225 $tokenProcessor->addMessage('money', '{contribution_recur.amount|crmMoney}', 'text/plain');
226 $tokenProcessor->addMessage('raw', '{contribution_recur.amount|raw}', 'text/plain');
227 $tokenProcessor->addMessage('moneyNumber', '{contribution_recur.amount|crmMoneyNumber}', 'text/plain');
228 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
229 $tokenProcessor->evaluate();
230 $this->assertEquals('€5,990.99', $tokenProcessor->getRow(0)->render('not_specified'));
231 $this->assertEquals('€5,990.99', $tokenProcessor->getRow(0)->render('money'));
232 $this->assertEquals('5990.99', $tokenProcessor->getRow(0)->render('raw'));
236 * Test money format tokens can respect passed in locale.
238 public function testMoneyFormat(): void
{
239 // Our 'migration' off configured thousand separators at the moment is a define.
240 putenv('IGNORE_SEPARATOR_CONFIG=1');
241 $this->createLoggedInUser();
242 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
243 'controller' => __CLASS__
,
245 'schema' => ['contribution_recurId'],
247 $tokenString = '{contribution_recur.amount}';
248 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
249 $tokenProcessor->addRow([
250 'contribution_recurId' => $this->getContributionRecurID(),
253 $tokenProcessor->evaluate();
254 $this->assertEquals('€ 5 990,99', $tokenProcessor->getRow(0)->render('html'));
258 * Get tokens that are not advertised via listTokens.
262 public function getUnadvertisedTokens(): array {
264 '{membership.status_id}' => 'Status ID',
265 '{membership.membership_type_id}' => 'Membership Type ID',
266 '{membership.status_id:name}' => 'Machine name: Status',
267 '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
268 '{contribution_recur.frequency_unit}' => 'Frequency Unit',
269 '{contribution_recur.contribution_status_id}' => 'Status',
270 '{contribution_recur.payment_processor_id}' => 'Payment Processor ID',
271 '{contribution_recur.financial_type_id}' => 'Financial Type ID',
272 '{contribution_recur.payment_instrument_id}' => 'Payment Method',
273 '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit',
274 '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method',
275 '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status',
276 '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor',
277 '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type',
278 '{participant.status_id:name}' => 'Machine name: Status',
279 '{participant.role_id:name}' => 'Machine name: Participant Role',
280 '{participant.status_id}' => 'Status ID',
281 '{participant.role_id}' => 'Participant Role ID',
286 * Test tokens in 2 ways to ensure consistent handling.
288 * 1) as part of the greeting processing
289 * 2) via the token processor.
292 public function testOddTokens(): void
{
296 'string' => '{contact.individual_prefix}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.individual_suffix}',
297 'expected' => 'Mr. Anthony Anderson II',
300 'string' => '{contact.prefix_id:label}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.suffix_id:label}',
301 'expected' => 'Mr. Anthony Anderson II',
304 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
306 'schema' => ['contactId'],
308 $contactID = $this->individualCreate(['middle_name' => '']);
309 $tokenProcessor->addRow(['contactId' => $contactID]);
310 foreach ($variants as $index => $variant) {
311 $tokenProcessor->addMessage($index, $variant['string'], 'text/plain');
313 $tokenProcessor->evaluate();
314 $result = $tokenProcessor->getRow(0);
315 foreach ($variants as $index => $variant) {
316 $greetingString = $variant['string'];
317 CRM_Utils_Token
::replaceGreetingTokens($greetingString, $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]), $contactID);
318 $this->assertEquals($variant['expected'], $greetingString, 'replaceGreetingTokens() should render expected output');
319 $this->assertEquals($variant['expected'], $result->render($index), 'TokenProcessor should render expected output');
324 * Get the contribution recur tokens keyed by the token.
326 * e.g {contribution_recur.id}
330 protected function getContributionRecurTokens(): array {
332 foreach ($this->getContributionRecurTokensByField() as $key => $value) {
333 $return['{contribution_recur.' . $key . '}'] = $value;
338 protected function getContributionRecurTokensByField(): array {
340 'id' => 'Recurring Contribution ID',
341 'amount' => 'Amount',
342 'currency' => 'Currency',
343 'frequency_unit' => 'Frequency Unit',
344 'frequency_interval' => 'Interval (number of units)',
345 'installments' => 'Number of Installments',
346 'start_date' => 'Start Date',
347 'create_date' => 'Created Date',
348 'modified_date' => 'Modified Date',
349 'cancel_date' => 'Cancel Date',
350 'cancel_reason' => 'Cancellation Reason',
351 'end_date' => 'Recurring Contribution End Date',
352 'processor_id' => 'Processor ID',
353 'payment_token_id' => 'Payment Token ID',
354 'trxn_id' => 'Transaction ID',
355 'invoice_id' => 'Invoice ID',
356 'contribution_status_id' => 'Status',
357 'is_test:label' => 'Test',
358 'cycle_day' => 'Cycle Day',
359 'next_sched_contribution_date' => 'Next Scheduled Contribution Date',
360 'failure_count' => 'Number of Failures',
361 'failure_retry_date' => 'Retry Failed Attempt Date',
362 'auto_renew:label' => 'Auto Renew',
363 'payment_processor_id' => 'Payment Processor ID',
364 'financial_type_id' => 'Financial Type ID',
365 'payment_instrument_id' => 'Payment Method',
366 'is_email_receipt:label' => 'Send email Receipt?',
367 'frequency_unit:label' => 'Frequency Unit',
368 'frequency_unit:name' => 'Machine name: Frequency Unit',
369 'contribution_status_id:label' => 'Status',
370 'contribution_status_id:name' => 'Machine name: Status',
371 'payment_processor_id:label' => 'Payment Processor',
372 'payment_processor_id:name' => 'Machine name: Payment Processor',
373 'financial_type_id:label' => 'Financial Type',
374 'financial_type_id:name' => 'Machine name: Financial Type',
375 'payment_instrument_id:label' => 'Payment Method',
376 'payment_instrument_id:name' => 'Machine name: Payment Method',
381 * Get contributionRecur ID.
385 protected function getContributionRecurID(): int {
386 if (!isset($this->contributionRecur
)) {
387 $paymentProcessorID = $this->processorCreate();
388 $this->contributionRecur
= $this->callAPISuccess('ContributionRecur', 'create', [
389 'contact_id' => $this->getContactID(),
391 'is_email_receipt' => 1,
392 'start_date' => '2021-07-23 15:39:20',
393 'end_date' => '2021-07-26 18:07:20',
394 'cancel_date' => '2021-08-19 09:12:45',
395 'next_sched_contribution_date' => '2021-09-08',
396 'cancel_reason' => 'Because',
399 'frequency_unit' => 'year',
400 'frequency_interval' => 2,
401 'installments' => 24,
402 'payment_instrument_id' => 'Check',
403 'financial_type_id' => 'Member dues',
404 'processor_id' => 'abc',
405 'payment_processor_id' => $paymentProcessorID,
407 'invoice_id' => 'inv123',
409 'failure_retry_date' => '2020-01-03',
413 'payment_token_id' => $this->callAPISuccess('PaymentToken', 'create', [
414 'contact_id' => $this->getContactID(),
416 'payment_processor_id' => $paymentProcessorID,
420 return $this->contributionRecur
['id'];
424 * Get rendered output for contribution tokens.
428 protected function getExpectedContributionRecurTokenOutPut(): string {
429 return 'contribution_recur.id :' . $this->getContributionRecurID() . '
430 contribution_recur.amount :€5,990.99
431 contribution_recur.currency :EUR
432 contribution_recur.frequency_unit :year
433 contribution_recur.frequency_interval :2
434 contribution_recur.installments :24
435 contribution_recur.start_date :July 23rd, 2021 3:39 PM
436 contribution_recur.create_date :' . CRM_Utils_Date
::customFormat($this->contributionRecur
['create_date']) . '
437 contribution_recur.modified_date :' . CRM_Utils_Date
::customFormat($this->contributionRecur
['modified_date']) . '
438 contribution_recur.cancel_date :August 19th, 2021 9:12 AM
439 contribution_recur.cancel_reason :Because
440 contribution_recur.end_date :July 26th, 2021 6:07 PM
441 contribution_recur.processor_id :abc
442 contribution_recur.payment_token_id :1
443 contribution_recur.trxn_id :123
444 contribution_recur.invoice_id :inv123
445 contribution_recur.contribution_status_id :2
446 contribution_recur.is_test:label :Yes
447 contribution_recur.cycle_day :15
448 contribution_recur.next_sched_contribution_date :September 8th, 2021
449 contribution_recur.failure_count :0
450 contribution_recur.failure_retry_date :January 3rd, 2020
451 contribution_recur.auto_renew:label :Yes
452 contribution_recur.payment_processor_id :1
453 contribution_recur.financial_type_id :2
454 contribution_recur.payment_instrument_id :4
455 contribution_recur.is_email_receipt:label :Yes
456 contribution_recur.frequency_unit:label :year
457 contribution_recur.frequency_unit:name :year
458 contribution_recur.contribution_status_id:label :Pending Label**
459 contribution_recur.contribution_status_id:name :Pending
460 contribution_recur.payment_processor_id:label :Dummy (test)
461 contribution_recur.payment_processor_id:name :Dummy_test
462 contribution_recur.financial_type_id:label :Member Dues
463 contribution_recur.financial_type_id:name :Member Dues
464 contribution_recur.payment_instrument_id:label :Check
465 contribution_recur.payment_instrument_id:name :Check
471 * Test that membership tokens are consistently rendered.
473 * @throws \API_Exception
475 public function testMembershipTokenConsistency(): void
{
476 $this->createLoggedInUser();
477 $this->restoreMembershipTypes();
478 $this->createCustomGroupWithFieldOfType(['extends' => 'Membership']);
479 $tokens = CRM_Core_SelectValues
::membershipTokens();
480 $expectedTokens = $this->getMembershipTokens();
481 $this->assertEquals($expectedTokens, $tokens);
482 $newStyleTokens = "\n{membership.status_id:label}\n{membership.membership_type_id:label}\n";
483 $tokenString = $newStyleTokens . implode("\n", array_keys($this->getMembershipTokens()));
485 $memberships = CRM_Utils_Token
::getMembershipTokenDetails([$this->getMembershipID()]);
486 $messageToken = CRM_Utils_Token
::getTokens($tokenString);
487 $tokenHtml = CRM_Utils_Token
::replaceEntityTokens('membership', $memberships[$this->getMembershipID()], $tokenString, $messageToken);
488 $this->assertEquals($this->getExpectedMembershipTokenOutput(), $tokenHtml);
490 // Custom fields work in the processor so test it....
491 $tokenString .= "\n{membership." . $this->getCustomFieldName('text') . '}';
492 // Now compare with scheduled reminder
493 $mut = new CiviMailUtils($this);
494 CRM_Utils_Time
::setTime('2007-01-22 15:00:00');
495 $this->callAPISuccess('action_schedule', 'create', [
500 'start_action_date' => 'membership_join_date',
501 'start_action_offset' => 1,
502 'start_action_condition' => 'after',
503 'start_action_unit' => 'day',
504 'body_html' => $tokenString,
506 $this->callAPISuccess('job', 'send_reminder', []);
507 $expected = $this->getExpectedMembershipTokenOutput();
508 // Unlike the legacy method custom fields are resolved by the processor.
509 $expected .= "\nmy field";
510 $mut->checkMailLog([$expected]);
512 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
513 'controller' => __CLASS__
,
515 'schema' => ['membershipId'],
517 $tokens = $tokenProcessor->listTokens();
518 // Add in custom tokens as token processor supports these.
519 $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy());
520 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
521 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
522 $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
523 $tokenProcessor->evaluate();
524 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
529 * Get the advertised tokens the legacy function doesn't know about.
533 public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array {
535 '{membership.custom_1}' => 'Enter text here :: Group with field text',
536 '{membership.source}' => 'Source',
537 '{membership.status_override_end_date}' => 'Status Override End Date',
542 * Get declared membership tokens.
546 public function getMembershipTokens(): array {
548 '{membership.id}' => 'Membership ID',
549 '{membership.status_id:label}' => 'Status',
550 '{membership.membership_type_id:label}' => 'Membership Type',
551 '{membership.start_date}' => 'Membership Start Date',
552 '{membership.join_date}' => 'Member Since',
553 '{membership.end_date}' => 'Membership Expiration Date',
554 '{membership.fee}' => 'Membership Fee',
563 protected function getMembershipID(): int {
564 if (!isset($this->ids
['Membership'][0])) {
565 $this->ids
['Membership'][0] = $this->contactMembershipCreate([
566 'contact_id' => $this->getContactID(),
567 $this->getCustomFieldName('text') => 'my field',
570 return $this->ids
['Membership'][0];
574 * Get expected output from token parsing.
578 protected function getExpectedParticipantTokenOutput(): string {
579 return 'participant.status_id :2
580 participant.role_id :1
581 participant.register_date :February 19th, 2007
582 participant.source :Wimbeldon
583 participant.fee_level :steep
584 participant.fee_amount :$50.00
585 participant.registered_by_id :
586 participant.transferred_to_contact_id :
587 participant.role_id:label :Attendee
588 participant.balance :
589 participant.custom_2 :99999
591 participant.fee_currency :USD
592 participant.discount_amount :
593 participant.status_id:label :Attended
594 participant.status_id:name :Attended
595 participant.role_id:name :Attendee
596 participant.is_test:label :No
597 participant.must_wait :
602 * Get expected output from token parsing.
606 protected function getExpectedEventTokenOutput(): string {
607 return 'event.id :' . $this->ids
['event'][0] . '
608 event.title :Annual CiviCRM meet
609 event.start_date :October 21st, 2008
610 event.end_date :October 23rd, 2008
611 event.event_type_id:label :Conference
612 event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
613 event.contact_email :event@example.com
614 event.contact_phone :456 789
615 event.description :event description
616 event.location :15 Walton St
617 Emerald City, Maine 90210
619 event.info_url :' . CRM_Utils_System
::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
620 event.registration_url :' . CRM_Utils_System
::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
621 event.custom_1 :my field
626 * Get expected output from token parsing.
630 protected function getExpectedMembershipTokenOutput(): string {
644 * Test that membership tokens are consistently rendered.
646 * @throws \API_Exception
648 public function testParticipantTokenConsistency(): void
{
649 $this->createLoggedInUser();
650 $this->setupParticipantScheduledReminder();
652 $tokens = CRM_Core_SelectValues
::participantTokens();
653 $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens);
655 $mut = new CiviMailUtils($this);
657 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
658 'controller' => __CLASS__
,
660 'schema' => ['participantId'],
662 $this->assertEquals(array_merge($tokens, $this->getEventTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
664 $this->callAPISuccess('job', 'send_reminder', []);
665 $expected = $this->getExpectedParticipantTokenOutput();
666 $mut->checkMailLog([$expected]);
668 $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
669 $tokenProcessor->addRow(['participantId' => $this->ids
['participant'][0]]);
670 $tokenProcessor->evaluate();
671 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
676 * Test that membership tokens are consistently rendered.
678 * @throws \API_Exception
679 * @throws \CRM_Core_Exception
681 public function testParticipantCustomDateToken(): void
{
682 $this->createEventAndParticipant();
683 $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids
['CustomGroup']['participant_'], 'default_value' => ''])['id'];
684 $input = '{participant.custom_' . $dateFieldID . '}';
685 $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
686 $tokenHtml = CRM_Core_BAO_MessageTemplate
::renderTemplate([
687 'messageTemplate' => ['msg_html' => $input],
688 'tokenContext' => array_merge(['participantId' => $this->ids
['participant'][0]], ['schema' => ['participantId', 'eventId']]),
690 $this->assertEquals(99999, $tokenHtml);
694 * Get declared participant tokens.
698 public function getParticipantTokens(): array {
700 '{participant.status_id}' => 'Status ID',
701 '{participant.role_id}' => 'Participant Role ID',
702 '{participant.register_date}' => 'Register date',
703 '{participant.source}' => 'Participant Source',
704 '{participant.fee_level}' => 'Fee level',
705 '{participant.fee_amount}' => 'Fee Amount',
706 '{participant.registered_by_id}' => 'Registered By Participant ID',
707 '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
708 '{participant.role_id:label}' => 'Participant Role',
709 '{participant.balance}' => 'Event Balance',
710 '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
711 '{participant.id}' => 'Participant ID',
712 '{participant.fee_currency}' => 'Fee Currency',
713 '{participant.discount_amount}' => 'Discount Amount',
714 '{participant.status_id:label}' => 'Status',
715 '{participant.status_id:name}' => 'Machine name: Status',
716 '{participant.role_id:name}' => 'Machine name: Participant Role',
717 '{participant.is_test:label}' => 'Test',
718 '{participant.must_wait}' => 'Must Wait on List',
723 * Test that domain tokens are consistently rendered.
725 public function testDomainTokenConsistency(): void
{
726 $tokens = CRM_Core_SelectValues
::domainTokens();
727 $this->assertEquals($this->getDomainTokens(), $tokens);
728 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
729 'controller' => __CLASS__
,
732 $tokens['{domain.id}'] = 'Domain ID';
733 $tokens['{domain.description}'] = 'Domain Description';
734 $tokens['{domain.now}'] = 'Current time/date';
735 $this->assertEquals($tokens, $tokenProcessor->listTokens());
739 * @throws \API_Exception
740 * @throws \CRM_Core_Exception
742 public function testDomainNow(): void
{
743 putenv('TIME_FUNC=frozen');
744 CRM_Utils_Time
::setTime('2021-09-18 23:58:00');
746 'shortdate' => '09/18/2021',
747 '%B %Y' => 'September 2021',
749 foreach ($modifiers as $filter => $expected) {
750 $resolved = CRM_Core_BAO_MessageTemplate
::renderTemplate([
751 'messageTemplate' => [
752 'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
755 $this->assertEquals($expected, $resolved);
757 $resolved = CRM_Core_BAO_MessageTemplate
::renderTemplate([
758 'messageTemplate' => [
759 'msg_text' => '{domain.now}',
762 $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
764 // This example is malformed - no quotes
766 $resolved = CRM_Core_BAO_MessageTemplate
::renderTemplate([
767 'messageTemplate' => [
768 'msg_text' => '{domain.now|crmDate:shortdate}',
771 $this->fail('Expected unquoted parameter to fail');
773 catch (\CRM_Core_Exception
$e) {
774 $this->assertRegExp(';Malformed token param;', $e->getMessage());
779 * Get declared participant tokens.
783 public function getDomainTokens(): array {
785 '{domain.name}' => ts('Domain name'),
786 '{domain.address}' => ts('Domain (organization) address'),
787 '{domain.phone}' => ts('Domain (organization) phone'),
788 '{domain.email}' => 'Domain (organization) email',
789 '{domain.id}' => ts('Domain ID'),
790 '{domain.description}' => ts('Domain Description'),
791 '{domain.now}' => 'Current time/date',
792 '{domain.tax_term}' => 'Sales tax term (e.g VAT)',
797 * Test that event tokens are consistently rendered.
799 * @throws \API_Exception
801 public function testEventTokenConsistency(): void
{
802 $mut = new CiviMailUtils($this);
803 $this->setupParticipantScheduledReminder();
805 $tokens = CRM_Core_SelectValues
::eventTokens();
806 $this->assertEquals(array_merge($this->getEventTokens()), $tokens);
807 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
808 'controller' => __CLASS__
,
810 'schema' => ['eventId'],
812 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
814 $expectedEventString = $this->getExpectedEventTokenOutput();
815 $this->callAPISuccess('job', 'send_reminder', []);
816 $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
817 $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
818 $toCheck[] = $expectedEventString;
819 $toCheck[] = $expectedParticipantString;
820 $mut->checkMailLog($toCheck);
821 $tokens = array_keys($this->getEventTokens());
822 $html = $this->getTokenString($tokens);
823 $tokenProcessor->addMessage('html', $html, 'text/plain');
824 $tokenProcessor->addRow(['eventId' => $this->ids
['event'][0]]);
825 $tokenProcessor->evaluate();
826 $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
830 * Test that event tokens work absent participant tokens.
832 * @throws \API_Exception
834 public function testEventTokenConsistencyNoParticipantTokens(): void
{
835 $mut = new CiviMailUtils($this);
836 $this->setupParticipantScheduledReminder(FALSE);
838 $this->callAPISuccess('job', 'send_reminder', []);
839 $expected = $this->getExpectedEventTokenOutput();
840 // Checking these individually is easier to decipher discrepancies
841 // but we also want to check in entirety.
842 $toCheck = explode("\n", $expected);
843 $toCheck[] = $expected;
844 $mut->checkMailLog($toCheck);
846 $tokenProcessor = new TokenProcessor(\Civi
::dispatcher(), [
847 'controller' => __CLASS__
,
849 'schema' => ['eventId'],
851 $html = $this->getTokenString(array_keys($this->getEventTokens()));
853 $tokenProcessor->addMessage('html', $html, 'text/plain');
854 $tokenProcessor->addRow(['eventId' => $this->ids
['event'][0]]);
855 $tokenProcessor->evaluate();
856 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
861 * Set up scheduled reminder for participants.
863 * @throws \API_Exception
865 public function setupParticipantScheduledReminder($includeParticipant = TRUE): void
{
866 $this->createEventAndParticipant();
867 $tokens = array_keys($this->getEventTokens());
868 if ($includeParticipant) {
869 $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
871 $html = $this->getTokenString($tokens);
872 CRM_Utils_Time
::setTime('2007-02-20 15:00:00');
873 $this->callAPISuccess('action_schedule', 'create', [
878 'start_action_date' => 'register_date',
879 'start_action_offset' => 1,
880 'start_action_condition' => 'after',
881 'start_action_unit' => 'day',
882 'body_html' => $html,
887 * Get expected event tokens.
891 protected function getEventTokens(): array {
893 '{event.id}' => 'Event ID',
894 '{event.title}' => 'Event Title',
895 '{event.start_date}' => 'Event Start Date',
896 '{event.end_date}' => 'Event End Date',
897 '{event.event_type_id:label}' => 'Event Type',
898 '{event.summary}' => 'Event Summary',
899 '{event.contact_email}' => 'Event Contact Email',
900 '{event.contact_phone}' => 'Event Contact Phone',
901 '{event.description}' => 'Event Description',
902 '{event.location}' => 'Event Location',
903 '{event.info_url}' => 'Event Info URL',
904 '{event.registration_url}' => 'Event Registration URL',
905 '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
910 * @param array $tokens
914 protected function getTokenString(array $tokens): string {
916 foreach ($tokens as $token) {
917 $html .= substr($token, 1, -1) . ' :' . $token . "\n";
923 * Create an event with a participant.
925 * @throws \API_Exception
927 protected function createEventAndParticipant(): void
{
928 $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
929 $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
930 $emailID = Email
::create()
931 ->setValues(['email' => 'event@example.com'])
934 $addressID = Address
::create()->setValues([
935 'street_address' => '15 Walton St',
936 'supplemental_address_1' => 'up the road',
937 'city' => 'Emerald City',
938 'state_province_id:label' => 'Maine',
939 'postal_code' => 90210,
940 ])->execute()->first()['id'];
941 $phoneID = Phone
::create()
942 ->setValues(['phone' => '456 789'])
946 $locationBlockID = LocBlock
::save(FALSE)->setRecords([
948 'email_id' => $emailID,
949 'address_id' => $addressID,
950 'phone_id' => $phoneID,
952 ])->execute()->first()['id'];
953 $this->ids
['event'][0] = $this->eventCreate([
954 'description' => 'event description',
955 $this->getCustomFieldName('text') => 'my field',
956 'loc_block_id' => $locationBlockID,
958 // Create an unrelated participant record so that the ids don't match.
959 // this prevents things working just because the id 'happens to be valid'
960 $this->participantCreate([
961 'register_date' => '2020-01-01',
962 'event_id' => $this->ids
['event'][0],
964 $this->ids
['participant'][0] = $this->participantCreate([
965 'event_id' => $this->ids
['event'][0],
967 'fee_level' => 'steep',
968 $this->getCustomFieldName('participant_int') => '99999',
972 public function testEscaping() {
974 $create = function(string $entity, array $record = []) use (&$autoClean) {
975 // It's convenient to use createTestObject(), but it doesn't reproduce the normal escaping rules from QuickForm/APIv3/APIv4.
976 CRM_Utils_API_HTMLInputCoder
::singleton()->encodeRow($record);
977 $dao = CRM_Core_DAO
::createTestObject(CRM_Core_DAO_AllCoreTables
::getFullName($entity), $record);
979 // We're not using transactions, and truncating 'contact' seems problematic, so we roll up our sleeves and cleanup each record...
980 $autoClean[] = CRM_Utils_AutoClean
::with(function() use ($entity, $dao) {
981 CRM_Core_DAO
::deleteTestObjects(CRM_Core_DAO_AllCoreTables
::getFullName($entity), ['id' => $dao->id
]);
988 $context['contactId'] = $create('Contact', [
989 'first_name' => '<b>ig</b>illy brackets',
991 $context['eventId'] = $create('Event', [
992 'title' => 'The Webinar',
993 'description' => '<p>Some online webinar thingy.</p> <p>Attendees will need to install the <a href="http://telefoo.example.com">TeleFoo</a> app.</p>',
996 $messages = $expected = [];
998 // The `first_name` does not allow HTML. Any funny characters are presented like literal text.
999 $messages['contact_text'] = 'Hello {contact.first_name}!';
1000 $expected['contact_text'] = 'Hello <b>ig</b>illy brackets!';
1002 $messages['contact_html'] = '<p>Hello {contact.first_name}!</p>';
1003 $expected['contact_html'] = '<p>Hello <b>ig</b>illy brackets!</p>';
1005 // The `description` does allow HTML. Any funny characters are filtered out of text.
1006 $messages['event_text'] = 'You signed up for this event: {event.title}: {event.description}';
1007 $expected['event_text'] = 'You signed up for this event: The Webinar: Some online webinar thingy. Attendees will need to install the TeleFoo app.';
1009 $messages['event_html'] = '<p>You signed up for this event:</p> <h3>{event.title}</h3> {event.description}';
1010 $expected['event_html'] = '<p>You signed up for this event:</p> <h3>The Webinar</h3> <p>Some online webinar thingy.</p> <p>Attendees will need to install the <a href="http://telefoo.example.com">TeleFoo</a> app.</p>';
1012 $rendered = CRM_Core_TokenSmarty
::render($messages, $context);
1014 $this->assertEquals($expected, $rendered);