Merge pull request #21790 from eileenmcnaughton/greetings_earthling
[civicrm-core.git] / tests / phpunit / CRM / Utils / TokenConsistencyTest.php
CommitLineData
d0ce76fd
EM
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
0f4031da 12use Civi\Token\TokenProcessor;
f399fbd8
EM
13use Civi\Api4\LocBlock;
14use Civi\Api4\Email;
15use Civi\Api4\Phone;
16use Civi\Api4\Address;
0f4031da 17
d0ce76fd
EM
18/**
19 * CRM_Utils_TokenConsistencyTest
20 *
21 * Class for ensuring tokens have internal consistency.
22 *
23 * @group Tokens
24 *
25 * @group headless
26 */
27class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
28
29 use CRMTraits_Custom_CustomDataTrait;
30
31 /**
32 * Created case.
33 *
34 * @var array
35 */
36 protected $case;
37
0f4031da
EM
38 /**
39 * Recurring contribution.
40 *
41 * @var array
42 */
43 protected $contributionRecur;
44
d0ce76fd
EM
45 /**
46 * Post test cleanup.
d0ce76fd
EM
47 */
48 public function tearDown(): void {
e80f2261 49 $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
f70a513f 50 $this->quickCleanUpFinancialEntities();
167f848f
TO
51
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();
55
d0ce76fd
EM
56 parent::tearDown();
57 }
58
59 /**
60 * Test that case tokens are consistently rendered.
61 *
d0ce76fd
EM
62 * @throws \CiviCRM_API3_Exception
63 */
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();
044c0ad1 71 $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
78ffc4d7 72 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
d0ce76fd
EM
73 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
74 // Now do the same without passing in 'knownTokens'
78ffc4d7 75 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString);
d0ce76fd 76 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
2f5c0408
EM
77
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);
78ffc4d7
EM
81 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
82 'controller' => __CLASS__,
83 'smarty' => FALSE,
84 'schema' => ['caseId'],
85 ]);
889b0617 86 $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
78ffc4d7
EM
87 $tokenProcessor->addRow([
88 'caseId' => $this->getCaseID(),
89 ]);
90 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
91
92 $tokenProcessor->evaluate();
93 foreach ($tokenProcessor->getRows() as $row) {
94 $text = $row->render('html');
95 }
96 $this->assertEquals($this->getExpectedCaseTokenOutput(), $text);
d0ce76fd
EM
97 }
98
99 /**
100 * Get expected output from token parsing.
101 *
102 * @return string
103 */
104 protected function getExpectedCaseTokenOutput(): string {
044c0ad1
EM
105 return 'case.id :1
106case.case_type_id:label :Housing Support
107case.subject :Case Subject
108case.start_date :July 23rd, 2021
109case.end_date :July 26th, 2021
110case.details :case details
111case.status_id:label :Ongoing
112case.is_deleted:label :No
113case.created_date :' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
114case.modified_date :' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
115case.custom_1 :' . '
d0ce76fd
EM
116';
117 }
118
119 /**
120 * @return int
121 */
122 protected function getContactID(): int {
123 if (!isset($this->ids['Contact'][0])) {
124 $this->ids['Contact'][0] = $this->individualCreate();
125 }
126 return $this->ids['Contact'][0];
127 }
128
129 /**
130 * Get the keys for the case tokens.
131 *
132 * @return array
133 */
134 public function getCaseTokenKeys(): array {
135 $return = [];
136 foreach (array_keys($this->getCaseTokens()) as $key) {
137 $return[] = substr($key, 6, -1);
138 }
139 return $return;
140 }
141
142 /**
143 * Get declared tokens.
144 *
145 * @return string[]
146 */
147 public function getCaseTokens(): array {
148 return [
149 '{case.id}' => 'Case ID',
2f5c0408 150 '{case.case_type_id:label}' => 'Case Type',
d0ce76fd
EM
151 '{case.subject}' => 'Case Subject',
152 '{case.start_date}' => 'Case Start Date',
153 '{case.end_date}' => 'Case End Date',
154 '{case.details}' => 'Details',
2f5c0408 155 '{case.status_id:label}' => 'Case Status',
78ffc4d7 156 '{case.is_deleted:label}' => 'Case is in the Trash',
d0ce76fd
EM
157 '{case.created_date}' => 'Created Date',
158 '{case.modified_date}' => 'Modified Date',
159 '{case.custom_1}' => 'Enter text here :: Group with field text',
160 ];
161 }
162
163 /**
164 * Get case ID.
165 *
166 * @return int
167 */
168 protected function getCaseID(): int {
169 if (!isset($this->case)) {
a0fb3068 170 $case_id = $this->callAPISuccess('Case', 'create', [
d0ce76fd
EM
171 'case_type_id' => 'housing_support',
172 'activity_subject' => 'Case Subject',
173 'client_id' => $this->getContactID(),
174 'status_id' => 1,
175 'subject' => 'Case Subject',
176 'start_date' => '2021-07-23 15:39:20',
a0fb3068 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.
d0ce76fd
EM
180 'end_date' => '2021-07-26 18:07:20',
181 'medium_id' => 2,
182 'details' => 'case details',
183 'activity_details' => 'blah blah',
184 'sequential' => 1,
a0fb3068 185 ])['id'];
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]);
d0ce76fd
EM
191 }
192 return $this->case['id'];
193 }
194
0f4031da
EM
195 /**
196 * Test that contribution recur tokens are consistently rendered.
197 */
198 public function testContributionRecurTokenConsistency(): void {
199 $this->createLoggedInUser();
200 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
201 'controller' => __CLASS__,
202 'smarty' => FALSE,
203 'schema' => ['contribution_recurId'],
204 ]);
889b0617
EM
205 $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens());
206 $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens());
044c0ad1 207 $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
0f4031da
EM
208
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'));
213 }
214
f70a513f
EM
215 /**
216 * Test money format tokens can respect passed in locale.
217 */
218 public function testMoneyFormat(): void {
219 // Our 'migration' off configured thousand separators at the moment is a define.
220 putenv('IGNORE_SEPARATOR_CONFIG=1');
221 $this->createLoggedInUser();
222 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
223 'controller' => __CLASS__,
224 'smarty' => FALSE,
225 'schema' => ['contribution_recurId'],
226 ]);
227 $tokenString = '{contribution_recur.amount}';
228 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
229 $tokenProcessor->addRow([
230 'contribution_recurId' => $this->getContributionRecurID(),
231 'locale' => 'nb_NO',
232 ]);
233 $tokenProcessor->evaluate();
234 $this->assertEquals('€ 5 990,99', $tokenProcessor->getRow(0)->render('html'));
235 }
236
e9841a51
EM
237 /**
238 * Get tokens that are not advertised via listTokens.
239 *
240 * @return string[]
241 */
242 public function getUnadvertisedTokens(): array {
243 return [
244 '{membership.status_id}' => 'Status ID',
245 '{membership.membership_type_id}' => 'Membership Type ID',
246 '{membership.status_id:name}' => 'Machine name: Status',
247 '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
889b0617
EM
248 '{contribution_recur.frequency_unit}' => 'Frequency Unit',
249 '{contribution_recur.contribution_status_id}' => 'Status',
250 '{contribution_recur.payment_processor_id}' => 'Payment Processor ID',
251 '{contribution_recur.financial_type_id}' => 'Financial Type ID',
252 '{contribution_recur.payment_instrument_id}' => 'Payment Method',
253 '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit',
254 '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method',
255 '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status',
256 '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor',
257 '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type',
258 '{participant.status_id:name}' => 'Machine name: Status',
259 '{participant.role_id:name}' => 'Machine name: Participant Role',
260 '{participant.status_id}' => 'Status ID',
261 '{participant.role_id}' => 'Participant Role ID',
e9841a51
EM
262 ];
263 }
264
d49e8eec
EM
265 /**
266 * Test tokens in 2 ways to ensure consistent handling.
267 *
268 * 1) as part of the greeting processing
269 * 2) via the token processor.
270 *
271 */
272 public function testOddTokens(): void {
273
274 $variants = [
275 [
276 'string' => '{contact.individual_prefix}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.individual_suffix}',
277 'expected' => 'Mr. Anthony Anderson II',
278 ],
279 [
280 'string' => '{contact.prefix_id:label}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.suffix_id:label}',
281 'expected' => 'Mr. Anthony Anderson II',
282 ],
283 ];
284 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
285 'smarty' => FALSE,
286 'schema' => ['contactId'],
287 ]);
288 $contactID = $this->individualCreate(['middle_name' => '']);
289 $tokenProcessor->addRow(['contactId' => $contactID]);
d49e8eec
EM
290 foreach ($variants as $index => $variant) {
291 $tokenProcessor->addMessage($index, $variant['string'], 'text/plain');
292 }
293 $tokenProcessor->evaluate();
294 $result = $tokenProcessor->getRow(0);
295 foreach ($variants as $index => $variant) {
296 $greetingString = $variant['string'];
297 CRM_Utils_Token::replaceGreetingTokens($greetingString, $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]), $contactID);
0bb18246
TO
298 $this->assertEquals($variant['expected'], $greetingString, 'replaceGreetingTokens() should render expected output');
299 $this->assertEquals($variant['expected'], $result->render($index), 'TokenProcessor should render expected output');
d49e8eec
EM
300 }
301 }
302
0f4031da
EM
303 /**
304 * Get the contribution recur tokens keyed by the token.
305 *
306 * e.g {contribution_recur.id}
307 *
308 * @return array
309 */
310 protected function getContributionRecurTokens(): array {
311 $return = [];
312 foreach ($this->getContributionRecurTokensByField() as $key => $value) {
313 $return['{contribution_recur.' . $key . '}'] = $value;
314 }
315 return $return;
316 }
317
318 protected function getContributionRecurTokensByField(): array {
319 return [
320 'id' => 'Recurring Contribution ID',
321 'amount' => 'Amount',
322 'currency' => 'Currency',
323 'frequency_unit' => 'Frequency Unit',
324 'frequency_interval' => 'Interval (number of units)',
325 'installments' => 'Number of Installments',
326 'start_date' => 'Start Date',
327 'create_date' => 'Created Date',
328 'modified_date' => 'Modified Date',
329 'cancel_date' => 'Cancel Date',
330 'cancel_reason' => 'Cancellation Reason',
331 'end_date' => 'Recurring Contribution End Date',
332 'processor_id' => 'Processor ID',
333 'payment_token_id' => 'Payment Token ID',
334 'trxn_id' => 'Transaction ID',
335 'invoice_id' => 'Invoice ID',
336 'contribution_status_id' => 'Status',
97ca72e4 337 'is_test:label' => 'Test',
0f4031da
EM
338 'cycle_day' => 'Cycle Day',
339 'next_sched_contribution_date' => 'Next Scheduled Contribution Date',
340 'failure_count' => 'Number of Failures',
341 'failure_retry_date' => 'Retry Failed Attempt Date',
97ca72e4 342 'auto_renew:label' => 'Auto Renew',
0f4031da
EM
343 'payment_processor_id' => 'Payment Processor ID',
344 'financial_type_id' => 'Financial Type ID',
345 'payment_instrument_id' => 'Payment Method',
97ca72e4 346 'is_email_receipt:label' => 'Send email Receipt?',
0f4031da
EM
347 'frequency_unit:label' => 'Frequency Unit',
348 'frequency_unit:name' => 'Machine name: Frequency Unit',
349 'contribution_status_id:label' => 'Status',
350 'contribution_status_id:name' => 'Machine name: Status',
351 'payment_processor_id:label' => 'Payment Processor',
352 'payment_processor_id:name' => 'Machine name: Payment Processor',
353 'financial_type_id:label' => 'Financial Type',
354 'financial_type_id:name' => 'Machine name: Financial Type',
355 'payment_instrument_id:label' => 'Payment Method',
356 'payment_instrument_id:name' => 'Machine name: Payment Method',
357 ];
358 }
359
360 /**
361 * Get contributionRecur ID.
362 *
363 * @return int
364 */
365 protected function getContributionRecurID(): int {
366 if (!isset($this->contributionRecur)) {
367 $paymentProcessorID = $this->processorCreate();
368 $this->contributionRecur = $this->callAPISuccess('ContributionRecur', 'create', [
369 'contact_id' => $this->getContactID(),
370 'status_id' => 1,
371 'is_email_receipt' => 1,
372 'start_date' => '2021-07-23 15:39:20',
373 'end_date' => '2021-07-26 18:07:20',
374 'cancel_date' => '2021-08-19 09:12:45',
34795e7a 375 'next_sched_contribution_date' => '2021-09-08',
0f4031da
EM
376 'cancel_reason' => 'Because',
377 'amount' => 5990.99,
378 'currency' => 'EUR',
379 'frequency_unit' => 'year',
380 'frequency_interval' => 2,
381 'installments' => 24,
382 'payment_instrument_id' => 'Check',
383 'financial_type_id' => 'Member dues',
384 'processor_id' => 'abc',
385 'payment_processor_id' => $paymentProcessorID,
386 'trxn_id' => 123,
387 'invoice_id' => 'inv123',
388 'sequential' => 1,
389 'failure_retry_date' => '2020-01-03',
390 'auto_renew' => 1,
391 'cycle_day' => '15',
392 'is_test' => TRUE,
393 'payment_token_id' => $this->callAPISuccess('PaymentToken', 'create', [
394 'contact_id' => $this->getContactID(),
395 'token' => 456,
396 'payment_processor_id' => $paymentProcessorID,
397 ])['id'],
398 ])['values'][0];
399 }
400 return $this->contributionRecur['id'];
401 }
402
403 /**
404 * Get rendered output for contribution tokens.
405 *
406 * @return string
407 */
408 protected function getExpectedContributionRecurTokenOutPut(): string {
044c0ad1 409 return 'contribution_recur.id :' . $this->getContributionRecurID() . '
f70a513f 410contribution_recur.amount :€5,990.99
044c0ad1
EM
411contribution_recur.currency :EUR
412contribution_recur.frequency_unit :year
413contribution_recur.frequency_interval :2
414contribution_recur.installments :24
415contribution_recur.start_date :July 23rd, 2021 3:39 PM
416contribution_recur.create_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
417contribution_recur.modified_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
418contribution_recur.cancel_date :August 19th, 2021 9:12 AM
419contribution_recur.cancel_reason :Because
420contribution_recur.end_date :July 26th, 2021 6:07 PM
421contribution_recur.processor_id :abc
422contribution_recur.payment_token_id :1
423contribution_recur.trxn_id :123
424contribution_recur.invoice_id :inv123
425contribution_recur.contribution_status_id :2
426contribution_recur.is_test:label :Yes
427contribution_recur.cycle_day :15
428contribution_recur.next_sched_contribution_date :September 8th, 2021
429contribution_recur.failure_count :0
430contribution_recur.failure_retry_date :January 3rd, 2020
431contribution_recur.auto_renew:label :Yes
432contribution_recur.payment_processor_id :1
433contribution_recur.financial_type_id :2
434contribution_recur.payment_instrument_id :4
435contribution_recur.is_email_receipt:label :Yes
436contribution_recur.frequency_unit:label :year
437contribution_recur.frequency_unit:name :year
438contribution_recur.contribution_status_id:label :Pending Label**
439contribution_recur.contribution_status_id:name :Pending
440contribution_recur.payment_processor_id:label :Dummy (test)
441contribution_recur.payment_processor_id:name :Dummy (test)
442contribution_recur.financial_type_id:label :Member Dues
443contribution_recur.financial_type_id:name :Member Dues
444contribution_recur.payment_instrument_id:label :Check
445contribution_recur.payment_instrument_id:name :Check
446';
447
0f4031da
EM
448 }
449
d41a5d53
EM
450 /**
451 * Test that membership tokens are consistently rendered.
452 *
453 * @throws \API_Exception
454 */
455 public function testMembershipTokenConsistency(): void {
456 $this->createLoggedInUser();
457 $this->restoreMembershipTypes();
458 $this->createCustomGroupWithFieldOfType(['extends' => 'Membership']);
459 $tokens = CRM_Core_SelectValues::membershipTokens();
d568dbe0
EM
460 $expectedTokens = $this->getMembershipTokens();
461 $this->assertEquals($expectedTokens, $tokens);
d41a5d53
EM
462 $newStyleTokens = "\n{membership.status_id:label}\n{membership.membership_type_id:label}\n";
463 $tokenString = $newStyleTokens . implode("\n", array_keys($this->getMembershipTokens()));
b024d6a1 464
d41a5d53
EM
465 $memberships = CRM_Utils_Token::getMembershipTokenDetails([$this->getMembershipID()]);
466 $messageToken = CRM_Utils_Token::getTokens($tokenString);
467 $tokenHtml = CRM_Utils_Token::replaceEntityTokens('membership', $memberships[$this->getMembershipID()], $tokenString, $messageToken);
468 $this->assertEquals($this->getExpectedMembershipTokenOutput(), $tokenHtml);
dd2f879a 469
b024d6a1
EM
470 // Custom fields work in the processor so test it....
471 $tokenString .= "\n{membership." . $this->getCustomFieldName('text') . '}';
dd2f879a
EM
472 // Now compare with scheduled reminder
473 $mut = new CiviMailUtils($this);
474 CRM_Utils_Time::setTime('2007-01-22 15:00:00');
475 $this->callAPISuccess('action_schedule', 'create', [
476 'title' => 'job',
477 'subject' => 'job',
478 'entity_value' => 1,
479 'mapping_id' => 4,
480 'start_action_date' => 'membership_join_date',
481 'start_action_offset' => 1,
482 'start_action_condition' => 'after',
483 'start_action_unit' => 'day',
484 'body_html' => $tokenString,
485 ]);
486 $this->callAPISuccess('job', 'send_reminder', []);
d568dbe0
EM
487 $expected = $this->getExpectedMembershipTokenOutput();
488 // Unlike the legacy method custom fields are resolved by the processor.
489 $expected .= "\nmy field";
490 $mut->checkMailLog([$expected]);
491
492 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
493 'controller' => __CLASS__,
494 'smarty' => FALSE,
495 'schema' => ['membershipId'],
496 ]);
497 $tokens = $tokenProcessor->listTokens();
498 // Add in custom tokens as token processor supports these.
889b0617 499 $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy());
3c78698e 500 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
d568dbe0
EM
501 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
502 $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
503 $tokenProcessor->evaluate();
504 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
505
d41a5d53
EM
506 }
507
889b0617
EM
508 /**
509 * Get the advertised tokens the legacy function doesn't know about.
510 *
511 * @return string[]
512 */
513 public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array {
514 return [
515 '{membership.custom_1}' => 'Enter text here :: Group with field text',
516 '{membership.source}' => 'Source',
517 '{membership.status_override_end_date}' => 'Status Override End Date',
518 ];
519 }
520
d41a5d53
EM
521 /**
522 * Get declared membership tokens.
523 *
524 * @return string[]
525 */
526 public function getMembershipTokens(): array {
527 return [
528 '{membership.id}' => 'Membership ID',
044c0ad1 529 '{membership.status_id:label}' => 'Status',
eac0a5bf 530 '{membership.membership_type_id:label}' => 'Membership Type',
d41a5d53 531 '{membership.start_date}' => 'Membership Start Date',
044c0ad1
EM
532 '{membership.join_date}' => 'Member Since',
533 '{membership.end_date}' => 'Membership Expiration Date',
d41a5d53
EM
534 '{membership.fee}' => 'Membership Fee',
535 ];
536 }
537
538 /**
539 * Get case ID.
540 *
541 * @return int
542 */
543 protected function getMembershipID(): int {
544 if (!isset($this->ids['Membership'][0])) {
b024d6a1
EM
545 $this->ids['Membership'][0] = $this->contactMembershipCreate([
546 'contact_id' => $this->getContactID(),
547 $this->getCustomFieldName('text') => 'my field',
548 ]);
d41a5d53
EM
549 }
550 return $this->ids['Membership'][0];
551 }
552
f399fbd8
EM
553 /**
554 * Get expected output from token parsing.
555 *
556 * @return string
557 */
e80f2261 558 protected function getExpectedParticipantTokenOutput(): string {
044c0ad1
EM
559 return 'participant.status_id :2
560participant.role_id :1
561participant.register_date :February 19th, 2007
562participant.source :Wimbeldon
563participant.fee_level :steep
f70a513f 564participant.fee_amount :$50.00
044c0ad1
EM
565participant.registered_by_id :
566participant.transferred_to_contact_id :
567participant.role_id:label :Attendee
568participant.balance :
569participant.custom_2 :99999
570participant.id :2
571participant.fee_currency :USD
572participant.discount_amount :
573participant.status_id:label :Attended
574participant.status_id:name :Attended
575participant.role_id:name :Attendee
576participant.is_test:label :No
577participant.must_wait :
e80f2261
EM
578';
579 }
580
581 /**
582 * Get expected output from token parsing.
583 *
584 * @return string
585 */
586 protected function getExpectedEventTokenOutput(): string {
044c0ad1
EM
587 return 'event.id :' . $this->ids['event'][0] . '
588event.title :Annual CiviCRM meet
589event.start_date :October 21st, 2008
590event.end_date :October 23rd, 2008
591event.event_type_id:label :Conference
592event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
593event.contact_email :event@example.com
594event.contact_phone :456 789
595event.description :event description
596event.location :15 Walton St
f399fbd8
EM
597Emerald City, Maine 90210
598
044c0ad1
EM
599event.info_url :' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
600event.registration_url :' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
601event.custom_1 :my field
602';
f399fbd8
EM
603 }
604
d41a5d53
EM
605 /**
606 * Get expected output from token parsing.
607 *
608 * @return string
609 */
610 protected function getExpectedMembershipTokenOutput(): string {
611 return '
612Expired
613General
6141
615Expired
616General
617January 21st, 2007
618January 21st, 2007
619December 21st, 2007
620100.00';
621 }
622
1ed50dc6
EM
623 /**
624 * Test that membership tokens are consistently rendered.
625 *
626 * @throws \API_Exception
627 */
628 public function testParticipantTokenConsistency(): void {
629 $this->createLoggedInUser();
e80f2261
EM
630 $this->setupParticipantScheduledReminder();
631
1ed50dc6 632 $tokens = CRM_Core_SelectValues::participantTokens();
889b0617 633 $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens);
e80f2261
EM
634
635 $mut = new CiviMailUtils($this);
636
637 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
638 'controller' => __CLASS__,
639 'smarty' => FALSE,
640 'schema' => ['participantId'],
641 ]);
642 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
643
644 $this->callAPISuccess('job', 'send_reminder', []);
645 $expected = $this->getExpectedParticipantTokenOutput();
646 $mut->checkMailLog([$expected]);
647
044c0ad1 648 $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
e80f2261
EM
649 $tokenProcessor->addRow(['participantId' => $this->ids['participant'][0]]);
650 $tokenProcessor->evaluate();
651 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
652
1ed50dc6
EM
653 }
654
2a7cae66
EM
655 /**
656 * Test that membership tokens are consistently rendered.
657 *
658 * @throws \API_Exception
659 * @throws \CRM_Core_Exception
660 */
661 public function testParticipantCustomDateToken(): void {
662 $this->createEventAndParticipant();
663 $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids['CustomGroup']['participant_'], 'default_value' => ''])['id'];
664 $input = '{participant.custom_' . $dateFieldID . '}';
665 $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
666 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
667 'messageTemplate' => ['msg_html' => $input],
668 'tokenContext' => array_merge(['participantId' => $this->ids['participant'][0]], ['schema' => ['participantId', 'eventId']]),
669 ])['html'];
670 $this->assertEquals(99999, $tokenHtml);
671 }
672
1ed50dc6 673 /**
3c78698e 674 * Get declared participant tokens.
1ed50dc6
EM
675 *
676 * @return string[]
677 */
678 public function getParticipantTokens(): array {
679 return [
b7472bd6 680 '{participant.status_id}' => 'Status ID',
e80f2261 681 '{participant.role_id}' => 'Participant Role ID',
b7472bd6
EM
682 '{participant.register_date}' => 'Register date',
683 '{participant.source}' => 'Participant Source',
684 '{participant.fee_level}' => 'Fee level',
685 '{participant.fee_amount}' => 'Fee Amount',
686 '{participant.registered_by_id}' => 'Registered By Participant ID',
1ed50dc6 687 '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
e80f2261
EM
688 '{participant.role_id:label}' => 'Participant Role',
689 '{participant.balance}' => 'Event Balance',
690 '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
691 '{participant.id}' => 'Participant ID',
692 '{participant.fee_currency}' => 'Fee Currency',
693 '{participant.discount_amount}' => 'Discount Amount',
694 '{participant.status_id:label}' => 'Status',
695 '{participant.status_id:name}' => 'Machine name: Status',
696 '{participant.role_id:name}' => 'Machine name: Participant Role',
697 '{participant.is_test:label}' => 'Test',
698 '{participant.must_wait}' => 'Must Wait on List',
1ed50dc6
EM
699 ];
700 }
701
3c78698e
EM
702 /**
703 * Test that domain tokens are consistently rendered.
704 */
705 public function testDomainTokenConsistency(): void {
706 $tokens = CRM_Core_SelectValues::domainTokens();
707 $this->assertEquals($this->getDomainTokens(), $tokens);
708 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
709 'controller' => __CLASS__,
710 'smarty' => FALSE,
711 ]);
712 $tokens['{domain.id}'] = 'Domain ID';
713 $tokens['{domain.description}'] = 'Domain Description';
dfe53edd 714 $tokens['{domain.now}'] = 'Current time/date';
3c78698e
EM
715 $this->assertEquals($tokens, $tokenProcessor->listTokens());
716 }
717
defba8ff
EM
718 /**
719 * @throws \API_Exception
720 * @throws \CRM_Core_Exception
721 */
722 public function testDomainNow(): void {
723 putenv('TIME_FUNC=frozen');
5e62af3d 724 CRM_Utils_Time::setTime('2021-09-18 23:58:00');
44dd64f0
EM
725 $modifiers = [
726 'shortdate' => '09/18/2021',
f85e1a4b 727 '%B %Y' => 'September 2021',
44dd64f0
EM
728 ];
729 foreach ($modifiers as $filter => $expected) {
730 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
731 'messageTemplate' => [
f85e1a4b 732 'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
44dd64f0
EM
733 ],
734 ])['text'];
f85e1a4b 735 $this->assertEquals($expected, $resolved);
44dd64f0 736 }
defba8ff
EM
737 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
738 'messageTemplate' => [
739 'msg_text' => '{domain.now}',
740 ],
741 ])['text'];
742 $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
f85e1a4b
TO
743
744 // This example is malformed - no quotes
745 try {
746 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
747 'messageTemplate' => [
748 'msg_text' => '{domain.now|crmDate:shortdate}',
749 ],
750 ])['text'];
e80f2261 751 $this->fail('Expected unquoted parameter to fail');
f85e1a4b
TO
752 }
753 catch (\CRM_Core_Exception $e) {
754 $this->assertRegExp(';Malformed token param;', $e->getMessage());
755 }
defba8ff
EM
756 }
757
3c78698e
EM
758 /**
759 * Get declared participant tokens.
760 *
761 * @return string[]
762 */
763 public function getDomainTokens(): array {
764 return [
765 '{domain.name}' => ts('Domain name'),
766 '{domain.address}' => ts('Domain (organization) address'),
767 '{domain.phone}' => ts('Domain (organization) phone'),
768 '{domain.email}' => 'Domain (organization) email',
769 '{domain.id}' => ts('Domain ID'),
770 '{domain.description}' => ts('Domain Description'),
defba8ff 771 '{domain.now}' => 'Current time/date',
3c78698e
EM
772 ];
773 }
774
ce971869 775 /**
e80f2261 776 * Test that event tokens are consistently rendered.
34795e7a
EM
777 *
778 * @throws \API_Exception
ce971869
EM
779 */
780 public function testEventTokenConsistency(): void {
f399fbd8
EM
781 $mut = new CiviMailUtils($this);
782 $this->setupParticipantScheduledReminder();
783
ce971869 784 $tokens = CRM_Core_SelectValues::eventTokens();
889b0617 785 $this->assertEquals(array_merge($this->getEventTokens()), $tokens);
ce971869
EM
786 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
787 'controller' => __CLASS__,
788 'smarty' => FALSE,
789 'schema' => ['eventId'],
790 ]);
791 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
f399fbd8 792
044c0ad1 793 $expectedEventString = $this->getExpectedEventTokenOutput();
e80f2261 794 $this->callAPISuccess('job', 'send_reminder', []);
044c0ad1
EM
795 $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
796 $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
797 $toCheck[] = $expectedEventString;
798 $toCheck[] = $expectedParticipantString;
799 $mut->checkMailLog($toCheck);
800 $tokens = array_keys($this->getEventTokens());
801 $html = $this->getTokenString($tokens);
802 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
803 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
804 $tokenProcessor->evaluate();
044c0ad1 805 $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
e80f2261
EM
806 }
807
808 /**
809 * Test that event tokens work absent participant tokens.
810 *
811 * @throws \API_Exception
812 */
813 public function testEventTokenConsistencyNoParticipantTokens(): void {
814 $mut = new CiviMailUtils($this);
815 $this->setupParticipantScheduledReminder(FALSE);
816
f399fbd8
EM
817 $this->callAPISuccess('job', 'send_reminder', []);
818 $expected = $this->getExpectedEventTokenOutput();
044c0ad1
EM
819 // Checking these individually is easier to decipher discrepancies
820 // but we also want to check in entirety.
821 $toCheck = explode("\n", $expected);
822 $toCheck[] = $expected;
823 $mut->checkMailLog($toCheck);
e80f2261
EM
824
825 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
826 'controller' => __CLASS__,
827 'smarty' => FALSE,
828 'schema' => ['eventId'],
829 ]);
044c0ad1 830 $html = $this->getTokenString(array_keys($this->getEventTokens()));
e80f2261 831
044c0ad1 832 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
833 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
834 $tokenProcessor->evaluate();
835 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
836
f399fbd8
EM
837 }
838
839 /**
840 * Set up scheduled reminder for participants.
841 *
842 * @throws \API_Exception
843 */
e80f2261 844 public function setupParticipantScheduledReminder($includeParticipant = TRUE): void {
2a7cae66 845 $this->createEventAndParticipant();
044c0ad1 846 $tokens = array_keys($this->getEventTokens());
e80f2261 847 if ($includeParticipant) {
044c0ad1 848 $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
e80f2261 849 }
044c0ad1 850 $html = $this->getTokenString($tokens);
f399fbd8
EM
851 CRM_Utils_Time::setTime('2007-02-20 15:00:00');
852 $this->callAPISuccess('action_schedule', 'create', [
853 'title' => 'job',
854 'subject' => 'job',
855 'entity_value' => 1,
856 'mapping_id' => 2,
857 'start_action_date' => 'register_date',
858 'start_action_offset' => 1,
859 'start_action_condition' => 'after',
860 'start_action_unit' => 'day',
e80f2261 861 'body_html' => $html,
f399fbd8 862 ]);
ce971869
EM
863 }
864
865 /**
866 * Get expected event tokens.
867 *
868 * @return string[]
869 */
870 protected function getEventTokens(): array {
871 return [
e80f2261 872 '{event.id}' => 'Event ID',
ce971869
EM
873 '{event.title}' => 'Event Title',
874 '{event.start_date}' => 'Event Start Date',
875 '{event.end_date}' => 'Event End Date',
e80f2261 876 '{event.event_type_id:label}' => 'Event Type',
ce971869
EM
877 '{event.summary}' => 'Event Summary',
878 '{event.contact_email}' => 'Event Contact Email',
879 '{event.contact_phone}' => 'Event Contact Phone',
880 '{event.description}' => 'Event Description',
881 '{event.location}' => 'Event Location',
ce971869
EM
882 '{event.info_url}' => 'Event Info URL',
883 '{event.registration_url}' => 'Event Registration URL',
f399fbd8 884 '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
ce971869
EM
885 ];
886 }
887
044c0ad1
EM
888 /**
889 * @param array $tokens
890 *
891 * @return string
892 */
893 protected function getTokenString(array $tokens): string {
894 $html = '';
895 foreach ($tokens as $token) {
896 $html .= substr($token, 1, -1) . ' :' . $token . "\n";
897 }
898 return $html;
899 }
900
2a7cae66
EM
901 /**
902 * Create an event with a participant.
903 *
904 * @throws \API_Exception
905 */
906 protected function createEventAndParticipant(): void {
907 $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
908 $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
909 $emailID = Email::create()
910 ->setValues(['email' => 'event@example.com'])
911 ->execute()
912 ->first()['id'];
913 $addressID = Address::create()->setValues([
914 'street_address' => '15 Walton St',
915 'supplemental_address_1' => 'up the road',
916 'city' => 'Emerald City',
917 'state_province_id:label' => 'Maine',
918 'postal_code' => 90210,
919 ])->execute()->first()['id'];
920 $phoneID = Phone::create()
921 ->setValues(['phone' => '456 789'])
922 ->execute()
923 ->first()['id'];
924
925 $locationBlockID = LocBlock::save(FALSE)->setRecords([
926 [
927 'email_id' => $emailID,
928 'address_id' => $addressID,
929 'phone_id' => $phoneID,
930 ],
931 ])->execute()->first()['id'];
932 $this->ids['event'][0] = $this->eventCreate([
933 'description' => 'event description',
934 $this->getCustomFieldName('text') => 'my field',
935 'loc_block_id' => $locationBlockID,
936 ])['id'];
937 // Create an unrelated participant record so that the ids don't match.
938 // this prevents things working just because the id 'happens to be valid'
939 $this->participantCreate([
940 'register_date' => '2020-01-01',
941 'event_id' => $this->ids['event'][0],
942 ]);
943 $this->ids['participant'][0] = $this->participantCreate([
944 'event_id' => $this->ids['event'][0],
945 'fee_amount' => 50,
946 'fee_level' => 'steep',
947 $this->getCustomFieldName('participant_int') => '99999',
948 ]);
949 }
950
b05a7c72 951 public function testEscaping() {
167f848f
TO
952 $autoClean = [];
953 $create = function(string $entity, array $record = []) use (&$autoClean) {
954 // It's convenient to use createTestObject(), but it doesn't reproduce the normal escaping rules from QuickForm/APIv3/APIv4.
b05a7c72 955 CRM_Utils_API_HTMLInputCoder::singleton()->encodeRow($record);
167f848f
TO
956 $dao = CRM_Core_DAO::createTestObject(CRM_Core_DAO_AllCoreTables::getFullName($entity), $record);
957
958 // We're not using transactions, and truncating 'contact' seems problematic, so we roll up our sleeves and cleanup each record...
959 $autoClean[] = CRM_Utils_AutoClean::with(function() use ($entity, $dao) {
960 CRM_Core_DAO::deleteTestObjects(CRM_Core_DAO_AllCoreTables::getFullName($entity), ['id' => $dao->id]);
961 });
962
963 return $dao;
b05a7c72
TO
964 };
965
966 $context = [];
967 $context['contactId'] = $create('Contact', [
968 'first_name' => '<b>ig</b>illy brackets',
969 ])->id;
970 $context['eventId'] = $create('Event', [
971 'title' => 'The Webinar',
972 'description' => '<p>Some online webinar thingy.</p> <p>Attendees will need to install the <a href="http://telefoo.example.com">TeleFoo</a> app.</p>',
973 ])->id;
974
975 $messages = $expected = [];
976
977 // The `first_name` does not allow HTML. Any funny characters are presented like literal text.
978 $messages['contact_text'] = 'Hello {contact.first_name}!';
979 $expected['contact_text'] = "Hello <b>ig</b>illy brackets!";
980
981 $messages['contact_html'] = "<p>Hello {contact.first_name}!</p>";
982 $expected['contact_html'] = "<p>Hello &lt;b&gt;ig&lt;/b&gt;illy brackets!</p>";
983
984 // The `description` does allow HTML. Any funny characters are filtered out of text.
985 $messages['event_text'] = 'You signed up for this event: {event.title}: {event.description}';
986 $expected['event_text'] = 'You signed up for this event: The Webinar: Some online webinar thingy. Attendees will need to install the TeleFoo app.';
987
988 $messages['event_html'] = "<p>You signed up for this event:</p> <h3>{event.title}</h3> {event.description}";
989 $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>';
990
991 $rendered = CRM_Core_TokenSmarty::render($messages, $context);
992
993 $this->assertEquals($expected, $rendered);
994 }
995
d0ce76fd 996}