Merge pull request #24148 from civicrm/5.52
[civicrm-core.git] / CRM / Contribute / Form / Task / PDFLetter.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035 11
31f2ebac
EM
12use Civi\Token\TokenProcessor;
13
6a488035
TO
14/**
15 *
16 * @package CRM
ca5cec67 17 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
18 */
19
20/**
07f8d162 21 * This class provides the functionality to create PDF letter for a group of contacts or a single contact.
6a488035
TO
22 */
23class CRM_Contribute_Form_Task_PDFLetter extends CRM_Contribute_Form_Task {
24
fc34a273
EM
25 use CRM_Contact_Form_Task_PDFTrait;
26
6a488035 27 /**
fe482240 28 * All the existing templates in the system.
6a488035
TO
29 *
30 * @var array
31 */
32 public $_templates = NULL;
33
34 public $_single = NULL;
35
36 public $_cid = NULL;
37
38 /**
fe482240 39 * Build all the data structures needed to build the form.
ed106721 40 */
00be9182 41 public function preProcess() {
6a488035 42 $this->skipOnHold = $this->skipDeceased = FALSE;
c97bfeff 43 $this->preProcessPDF();
1a90603e 44 parent::preProcess();
45 $this->assign('single', $this->isSingle());
6a488035
TO
46 }
47
186c9c17
EM
48 /**
49 * This virtual function is used to set the default values of
50 * various form elements
51 *
52 * access public
53 *
a6c01b45
CW
54 * @return array
55 * reference to the array of default values
186c9c17 56 */
1330f57a 57
186c9c17
EM
58 /**
59 * @return array
60 */
00be9182 61 public function setDefaultValues() {
fc34a273 62 $defaults = $this->getPDFDefaultValues();
6a488035 63 if (isset($this->_activityId)) {
be2fb01f 64 $params = ['id' => $this->_activityId];
6a488035 65 CRM_Activity_BAO_Activity::retrieve($params, $defaults);
9c1bc317 66 $defaults['html_message'] = $defaults['details'] ?? NULL;
6a488035 67 }
b5fd2bef
CW
68 else {
69 $defaults['thankyou_update'] = 1;
70 }
6a488035
TO
71 return $defaults;
72 }
73
74 /**
fe482240 75 * Build the form object.
053c1a4b
EM
76 *
77 * @throws \CRM_Core_Exception
6a488035
TO
78 */
79 public function buildQuickForm() {
80 //enable form element
81 $this->assign('suppressForm', FALSE);
82
b701d2e5 83 // Contribute PDF tasks allow you to email as well, so we need to add email address to those forms
84 $this->add('select', 'from_email_address', ts('From Email Address'), $this->_fromEmails, TRUE);
053c1a4b 85 $this->addPDFElementsToForm();
6a488035
TO
86
87 // specific need for contributions
383c047b 88 $this->add('static', 'more_options_header', NULL, ts('Thank-you Letter Options'));
6a488035
TO
89 $this->add('checkbox', 'receipt_update', ts('Update receipt dates for these contributions'), FALSE);
90 $this->add('checkbox', 'thankyou_update', ts('Update thank-you dates for these contributions'), FALSE);
6a488035
TO
91
92 // Group options for tokens are not yet implemented. dgg
be2fb01f 93 $options = [
383c047b
DG
94 '' => ts('- no grouping -'),
95 'contact_id' => ts('Contact'),
96 'contribution_recur_id' => ts('Contact and Recurring'),
97 'financial_type_id' => ts('Contact and Financial Type'),
98 'campaign_id' => ts('Contact and Campaign'),
536f0e02 99 'payment_instrument_id' => ts('Contact and Payment Method'),
be2fb01f
CW
100 ];
101 $this->addElement('select', 'group_by', ts('Group contributions by'), $options, [], "<br/>", FALSE);
383c047b 102 // this was going to be free-text but I opted for radio options in case there was a script injection risk
be2fb01f 103 $separatorOptions = ['comma' => 'Comma', 'td' => 'Horizontal Table Cell', 'tr' => 'Vertical Table Cell', 'br' => 'Line Break'];
698253db 104
383c047b 105 $this->addElement('select', 'group_by_separator', ts('Separator (grouped contributions)'), $separatorOptions);
be2fb01f 106 $emailOptions = [
383c047b
DG
107 '' => ts('Generate PDFs for printing (only)'),
108 'email' => ts('Send emails where possible. Generate printable PDFs for contacts who cannot receive email.'),
109 'both' => ts('Send emails where possible. Generate printable PDFs for all contacts.'),
be2fb01f 110 ];
353ffa53 111 if (CRM_Core_Config::singleton()->doNotAttachPDFReceipt) {
97eaa51b
EM
112 $emailOptions['pdfemail'] = ts('Send emails with an attached PDF where possible. Generate printable PDFs for contacts who cannot receive email.');
113 $emailOptions['pdfemail_both'] = ts('Send emails with an attached PDF where possible. Generate printable PDFs for all contacts.');
114 }
be2fb01f 115 $this->addElement('select', 'email_options', ts('Print and email options'), $emailOptions, [], "<br/>", FALSE);
ed106721 116
f336046d 117 $this->addButtons($this->getButtons());
6a488035
TO
118
119 }
120
f336046d
EM
121 /**
122 * Get the name for the main submit button.
123 *
124 * @return string
125 */
126 protected function getMainSubmitButtonName(): string {
127 return ts('Make Thank-you Letters');
128 }
129
6a488035 130 /**
fe482240 131 * Process the form after the input has been submitted and validated.
9da59513 132 *
133 * @throws \CRM_Core_Exception
cf56c730 134 * @throws \CiviCRM_API3_Exception
6a488035
TO
135 */
136 public function postProcess() {
9be8686d 137 $formValues = $this->controller->exportValues($this->getName());
31f2ebac
EM
138 [$formValues, $html_message] = $this->processMessageTemplate($formValues);
139
140 $messageToken = CRM_Utils_Token::getTokens($html_message);
141
142 $returnProperties = [];
143 if (isset($messageToken['contact'])) {
144 foreach ($messageToken['contact'] as $key => $value) {
145 $returnProperties[$value] = 1;
146 }
147 }
148
b701d2e5 149 $isPDF = FALSE;
150 $emailParams = [];
151 if (!empty($formValues['email_options'])) {
152 $returnProperties['email'] = $returnProperties['on_hold'] = $returnProperties['is_deceased'] = $returnProperties['do_not_email'] = 1;
153 $emailParams = [
154 'subject' => $formValues['subject'] ?? NULL,
155 'from' => $formValues['from_email_address'] ?? NULL,
156 ];
157
158 $emailParams['from'] = CRM_Utils_Mail::formatFromAddress($emailParams['from']);
159
160 // We need display_name for emailLetter() so add to returnProperties here
161 $returnProperties['display_name'] = 1;
162 if (stristr($formValues['email_options'], 'pdfemail')) {
163 $isPDF = TRUE;
164 }
165 }
166 // update dates ?
167 $receipt_update = $formValues['receipt_update'] ?? FALSE;
168 $thankyou_update = $formValues['thankyou_update'] ?? FALSE;
169 $nowDate = date('YmdHis');
170 $receipts = $thanks = $emailed = 0;
171 $updateStatus = '';
172 $task = 'CRM_Contribution_Form_Task_PDFLetterCommon';
173 $realSeparator = ', ';
174 $tableSeparators = [
175 'td' => '</td><td>',
176 'tr' => '</td></tr><tr><td>',
177 ];
178 //the original thinking was mutliple options - but we are going with only 2 (comma & td) for now in case
179 // there are security (& UI) issues we need to think through
180 if (isset($formValues['group_by_separator'])) {
181 if (in_array($formValues['group_by_separator'], ['td', 'tr'])) {
182 $realSeparator = $tableSeparators[$formValues['group_by_separator']];
183 }
184 elseif ($formValues['group_by_separator'] == 'br') {
185 $realSeparator = "<br />";
186 }
187 }
188 // a placeholder in case the separator is common in the string - e.g ', '
189 $separator = '****~~~~';
9da59513 190 $groupBy = $this->getSubmittedValue('group_by');
b701d2e5 191
192 // skip some contacts ?
3467497b 193 $skipOnHold = $this->skipOnHold ?? FALSE;
194 $skipDeceased = $this->skipDeceased ?? TRUE;
9da59513 195 $contributionIDs = $this->getIDs();
3467497b 196 if ($this->isQueryIncludesSoftCredits()) {
b701d2e5 197 $contributionIDs = [];
3467497b 198 $result = $this->getSearchQueryResults();
b701d2e5 199 while ($result->fetch()) {
3467497b 200 $this->_contactIds[$result->contact_id] = $result->contact_id;
b701d2e5 201 $contributionIDs["{$result->contact_id}-{$result->contribution_id}"] = $result->contribution_id;
202 }
203 }
3467497b 204 [$contributions, $contacts] = $this->buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $this->isQueryIncludesSoftCredits());
b701d2e5 205 $html = [];
206 $contactHtml = $emailedHtml = [];
207 foreach ($contributions as $contributionId => $contribution) {
208 $contact = &$contacts[$contribution['contact_id']];
209 $grouped = FALSE;
210 $groupByID = 0;
211 if ($groupBy) {
212 $groupByID = empty($contribution[$groupBy]) ? 0 : $contribution[$groupBy];
213 $contribution = $contact['combined'][$groupBy][$groupByID];
214 $grouped = TRUE;
215 }
216
217 if (empty($groupBy) || empty($contact['is_sent'][$groupBy][$groupByID])) {
3467497b 218 $html[$contributionId] = $this->generateHtml($contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID);
b701d2e5 219 $contactHtml[$contact['contact_id']][] = $html[$contributionId];
f336046d 220 if ($this->isSendEmails()) {
3467497b 221 if ($this->emailLetter($contact, $html[$contributionId], $isPDF, $formValues, $emailParams)) {
b701d2e5 222 $emailed++;
223 if (!stristr($formValues['email_options'], 'both')) {
224 $emailedHtml[$contributionId] = TRUE;
225 }
226 }
227 }
228 $contact['is_sent'][$groupBy][$groupByID] = TRUE;
229 }
f336046d
EM
230 if ($this->isLiveMode()) {
231 // Update receipt/thankyou dates
232 $contributionParams = ['id' => $contributionId];
233 if ($receipt_update) {
234 $contributionParams['receipt_date'] = $nowDate;
235 }
236 if ($thankyou_update) {
237 $contributionParams['thankyou_date'] = $nowDate;
238 }
239 if ($receipt_update || $thankyou_update) {
240 civicrm_api3('Contribution', 'create', $contributionParams);
241 $receipts = ($receipt_update ? $receipts + 1 : $receipts);
242 $thanks = ($thankyou_update ? $thanks + 1 : $thanks);
243 }
b701d2e5 244 }
245 }
246
247 $contactIds = array_keys($contacts);
f336046d
EM
248 // CRM-16725 Skip creation of activities if user is previewing their PDF letter(s)
249 if ($this->isLiveMode()) {
2537798f 250 $this->createActivities($html_message, $contactIds, CRM_Utils_Array::value('subject', $formValues, ts('Thank you letter')), CRM_Utils_Array::value('campaign_id', $formValues), $contactHtml);
f336046d 251 }
b701d2e5 252 $html = array_diff_key($html, $emailedHtml);
253
b701d2e5 254 //CRM-19761
255 if (!empty($html)) {
7c0d6f7a 256 $fileName = $this->getFileName();
ba7ea395
JF
257 if ($this->getSubmittedValue('document_type') === 'pdf') {
258 CRM_Utils_PDF_Utils::html2pdf($html, $fileName . '.pdf', FALSE, $formValues);
b701d2e5 259 }
260 else {
ba7ea395 261 CRM_Utils_PDF_Document::html2doc($html, $fileName . '.' . $this->getSubmittedValue('document_type'), $formValues);
b701d2e5 262 }
263 }
264
3467497b 265 $this->postProcessHook();
b701d2e5 266
267 if ($emailed) {
268 $updateStatus = ts('Receipts have been emailed to %1 contributions.', [1 => $emailed]);
269 }
270 if ($receipts) {
271 $updateStatus = ts('Receipt date has been updated for %1 contributions.', [1 => $receipts]);
272 }
273 if ($thanks) {
274 $updateStatus .= ' ' . ts('Thank-you date has been updated for %1 contributions.', [1 => $thanks]);
275 }
276
277 if ($updateStatus) {
278 CRM_Core_Session::setStatus($updateStatus);
279 }
280 if (!empty($html)) {
281 // ie. we have only sent emails - lets no show a white screen
282 CRM_Utils_System::civiExit();
283 }
6a488035 284 }
96025800 285
f336046d
EM
286 /**
287 * Are emails to be sent out?
288 *
289 * @return bool
290 */
291 protected function isSendEmails(): bool {
292 return $this->isLiveMode() && $this->getSubmittedValue('email_options');
293 }
294
5ec6b0ad 295 /**
601c941f 296 * Get the token processor schema required to list any tokens for this task.
5ec6b0ad
TM
297 *
298 * @return array
299 */
601c941f
EM
300 public function getTokenSchema(): array {
301 return ['contributionId', 'contactId'];
5ec6b0ad
TM
302 }
303
01a5461d 304 /**
305 * Generate the contribution array from the form, we fill in the contact details and determine any aggregation
306 * around contact_id of contribution_recur_id
307 *
308 * @param string $groupBy
309 * @param array $contributionIDs
310 * @param array $returnProperties
311 * @param bool $skipOnHold
312 * @param bool $skipDeceased
313 * @param array $messageToken
314 * @param string $task
315 * @param string $separator
316 * @param bool $isIncludeSoftCredits
317 *
318 * @return array
14145505 319 * @throws \CiviCRM_API3_Exception
01a5461d 320 */
3467497b 321 public function buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $isIncludeSoftCredits) {
01a5461d 322 $contributions = $contacts = [];
323 foreach ($contributionIDs as $item => $contributionId) {
324 $contribution = CRM_Contribute_BAO_Contribution::getContributionTokenValues($contributionId, $messageToken)['values'][$contributionId];
325 $contribution['campaign'] = $contribution['contribution_campaign_title'] ?? NULL;
326 $contributions[$contributionId] = $contribution;
327
328 if ($isIncludeSoftCredits) {
329 //@todo find out why this happens & add comments
330 [$contactID] = explode('-', $item);
331 $contactID = (int) $contactID;
332 }
333 else {
334 $contactID = $contribution['contact_id'];
335 }
336 if (!isset($contacts[$contactID])) {
337 $contacts[$contactID] = [];
338 $contacts[$contactID]['contact_aggregate'] = 0;
339 $contacts[$contactID]['combined'] = $contacts[$contactID]['contribution_ids'] = [];
340 }
341
342 $contacts[$contactID]['contact_aggregate'] += $contribution['total_amount'];
343 $groupByID = empty($contribution[$groupBy]) ? 0 : $contribution[$groupBy];
344
345 $contacts[$contactID]['contribution_ids'][$groupBy][$groupByID][$contributionId] = TRUE;
346 if (!isset($contacts[$contactID]['combined'][$groupBy]) || !isset($contacts[$contactID]['combined'][$groupBy][$groupByID])) {
347 $contacts[$contactID]['combined'][$groupBy][$groupByID] = $contribution;
348 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] = $contribution['total_amount'];
349 }
350 else {
351 $contacts[$contactID]['combined'][$groupBy][$groupByID] = self::combineContributions($contacts[$contactID]['combined'][$groupBy][$groupByID], $contribution, $separator);
352 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] += $contribution['total_amount'];
353 }
354 }
355 // Assign the available contributions before calling tokens so hooks parsing smarty can access it.
356 // Note that in core code you can only use smarty here if enable if for the whole site, incl
357 // CiviMail, with a big performance impact.
358 // Hooks allow more nuanced smarty usage here.
359 CRM_Core_Smarty::singleton()->assign('contributions', $contributions);
14145505
EM
360 $resolvedContacts = civicrm_api3('Contact', 'get', [
361 'return' => array_keys($returnProperties),
362 'id' => ['IN' => array_keys($contacts)],
a5ba7508 363 'options' => ['limit' => 0],
14145505 364 ])['values'];
01a5461d 365 foreach ($contacts as $contactID => $contact) {
14145505 366 $contacts[$contactID] = array_merge($resolvedContacts[$contactID], $contact);
01a5461d 367 }
368 return [$contributions, $contacts];
369 }
370
371 /**
372 * We combine the contributions by adding the contribution to each field with the separator in
373 * between the existing value and the new one. We put the separator there even if empty so it is clear what the
374 * value for previous contributions was
375 *
376 * @param array $existing
377 * @param array $contribution
378 * @param string $separator
379 *
380 * @return array
381 */
382 public static function combineContributions($existing, $contribution, $separator) {
383 foreach ($contribution as $field => $value) {
384 $existing[$field] = isset($existing[$field]) ? $existing[$field] . $separator : '';
385 $existing[$field] .= $value;
386 }
387 return $existing;
388 }
389
390 /**
391 * We are going to retrieve the combined contribution and if smarty mail is enabled we
392 * will also assign an array of contributions for this contact to the smarty template
393 *
394 * @param array $contact
395 * @param array $contributions
396 * @param $groupBy
397 * @param int $groupByID
398 */
399 public static function assignCombinedContributionValues($contact, $contributions, $groupBy, $groupByID) {
400 CRM_Core_Smarty::singleton()->assign('contact_aggregate', $contact['contact_aggregate']);
401 CRM_Core_Smarty::singleton()
402 ->assign('contributions', $contributions);
403 CRM_Core_Smarty::singleton()->assign('contribution_aggregate', $contact['aggregates'][$groupBy][$groupByID]);
404 }
405
406 /**
407 * @param $contact
408 * @param $formValues
409 * @param $contribution
410 * @param $groupBy
411 * @param $contributions
412 * @param $realSeparator
413 * @param $tableSeparators
414 * @param $messageToken
415 * @param $html_message
416 * @param $separator
417 * @param $categories
418 * @param bool $grouped
419 * @param int $groupByID
420 *
421 * @return string
422 * @throws \CRM_Core_Exception
423 */
9292a691 424 public function generateHtml($contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID) {
01a5461d 425 static $validated = FALSE;
426 $html = NULL;
427
428 $groupedContributions = array_intersect_key($contributions, $contact['contribution_ids'][$groupBy][$groupByID]);
429 CRM_Contribute_Form_Task_PDFLetter::assignCombinedContributionValues($contact, $groupedContributions, $groupBy, $groupByID);
430
431 if (empty($groupBy) || empty($contact['is_sent'][$groupBy][$groupByID])) {
b701d2e5 432 if (!$validated && in_array($realSeparator, $tableSeparators) && !CRM_Contribute_Form_Task_PDFLetter::isValidHTMLWithTableSeparator($messageToken, $html_message)) {
01a5461d 433 $realSeparator = ', ';
434 CRM_Core_Session::setStatus(ts('You have selected the table cell separator, but one or more token fields are not placed inside a table cell. This would result in invalid HTML, so comma separators have been used instead.'));
435 }
436 $validated = TRUE;
9292a691 437 $html = str_replace($separator, $realSeparator, $this->resolveTokens($html_message, $contact['contact_id'], $contribution['id'], $grouped, $separator, $groupedContributions));
01a5461d 438 }
439
440 return $html;
441 }
442
9be8686d 443 /**
444 * Send pdf by email.
445 *
446 * @param array $contact
447 * @param string $html
448 *
449 * @param $is_pdf
450 * @param array $format
451 * @param array $params
452 *
453 * @return bool
454 */
3467497b 455 public function emailLetter($contact, $html, $is_pdf, $format = [], $params = []) {
9be8686d 456 try {
457 if (empty($contact['email'])) {
458 return FALSE;
459 }
460 $mustBeEmpty = ['do_not_email', 'is_deceased', 'on_hold'];
461 foreach ($mustBeEmpty as $emptyField) {
462 if (!empty($contact[$emptyField])) {
463 return FALSE;
464 }
465 }
466
467 $defaults = [
52dd4e08 468 'contactId' => $contact['id'],
9be8686d 469 'toName' => $contact['display_name'],
470 'toEmail' => $contact['email'],
471 'text' => '',
472 'html' => $html,
473 ];
474 if (empty($params['from'])) {
475 $emails = CRM_Core_BAO_Email::getFromEmail();
476 $emails = array_keys($emails);
477 $defaults['from'] = array_pop($emails);
478 }
479 else {
480 $defaults['from'] = $params['from'];
481 }
482 if (!empty($params['subject'])) {
483 $defaults['subject'] = $params['subject'];
484 }
485 else {
486 $defaults['subject'] = ts('Thank you for your contribution/s');
487 }
488 if ($is_pdf) {
489 $defaults['html'] = ts('Please see attached');
490 $defaults['attachments'] = [CRM_Utils_Mail::appendPDF('ThankYou.pdf', $html, $format)];
491 }
8c1008e0 492 return CRM_Utils_Mail::send($defaults);
9be8686d 493 }
494 catch (CRM_Core_Exception $e) {
495 return FALSE;
496 }
497 }
498
499 /**
500 * Check that the token only appears in a table cell. The '</td><td>' separator cannot otherwise work
501 * Calculate the number of times it appears IN the cell & the number of times it appears - should be the same!
502 *
503 * @param string $token
504 * @param string $entity
505 * @param string $textToSearch
506 *
507 * @return bool
508 */
509 public static function isHtmlTokenInTableCell($token, $entity, $textToSearch) {
510 $tokenToMatch = $entity . '\.' . $token;
511 $pattern = '|<td(?![\w-])((?!</td>).)*\{' . $tokenToMatch . '\}.*?</td>|si';
512 $within = preg_match_all($pattern, $textToSearch);
513 $total = preg_match_all("|{" . $tokenToMatch . "}|", $textToSearch);
514 return ($within == $total);
515 }
516
b701d2e5 517 /**
518 * Check whether any of the tokens exist in the html outside a table cell.
519 * If they do the table cell separator is not supported (return false)
520 * At this stage we are only anticipating contributions passed in this way but
521 * it would be easy to add others
522 * @param $tokens
523 * @param $html
524 *
525 * @return bool
526 */
527 public static function isValidHTMLWithTableSeparator($tokens, $html) {
528 $relevantEntities = ['contribution'];
529 foreach ($relevantEntities as $entity) {
530 if (isset($tokens[$entity]) && is_array($tokens[$entity])) {
531 foreach ($tokens[$entity] as $token) {
532 if (!CRM_Contribute_Form_Task_PDFLetter::isHtmlTokenInTableCell($token, $entity, $html)) {
533 return FALSE;
534 }
535 }
536 }
537 }
538 return TRUE;
539 }
540
12d88807 541 /**
542 *
543 * @param string $html_message
9292a691 544 * @param int $contactID
31f2ebac 545 * @param int $contributionID
12d88807 546 * @param bool $grouped
547 * Does this letter represent more than one contribution.
548 * @param string $separator
549 * What is the preferred letter separator.
550 * @param array $contributions
551 *
552 * @return string
553 */
9292a691 554 protected function resolveTokens(string $html_message, int $contactID, $contributionID, $grouped, $separator, $contributions): string {
82430f99
TO
555 $tokenContext = [
556 'smarty' => (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY),
9292a691 557 'contactId' => $contactID,
31f2ebac 558 'schema' => ['contributionId'],
82430f99 559 ];
9a88bc66 560 if ($grouped) {
31f2ebac
EM
561 // First replace the contribution tokens. These are pretty ... special.
562 // if the text looks like `<td>{contribution.currency} {contribution.total_amount}</td>'
563 // and there are 2 rows with a currency separator of
564 // you wind up with a string like
565 // '<td>USD</td><td>USD></td> <td>$50</td><td>$89</td>
566 // see https://docs.civicrm.org/user/en/latest/contributions/manual-receipts-and-thank-yous/#grouped-contribution-thank-you-letters
567 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), $tokenContext);
568 $contributionTokens = CRM_Utils_Token::getTokens($html_message)['contribution'] ?? [];
569 foreach ($contributionTokens as $token) {
570 $tokenProcessor->addMessage($token, '{contribution.' . $token . '}', 'text/html');
571 }
572
573 foreach ($contributions as $contribution) {
574 $tokenProcessor->addRow([
575 'contributionId' => $contribution['id'],
576 'contribution' => $contribution,
577 ]);
578 }
579 $tokenProcessor->evaluate();
580 $resolvedTokens = [];
581 foreach ($contributionTokens as $token) {
582 foreach ($tokenProcessor->getRows() as $row) {
583 $resolvedTokens[$token][$row->context['contributionId']] = $row->render($token);
584 }
f70a513f 585 $html_message = str_replace('{contribution.' . $token . '}', implode($separator, $resolvedTokens[$token]), $html_message);
31f2ebac 586 }
9a88bc66 587 }
31f2ebac
EM
588 $tokenContext['contributionId'] = $contributionID;
589 return CRM_Core_TokenSmarty::render(['html' => $html_message], $tokenContext)['html'];
12d88807 590 }
591
6a488035 592}