4 * This class provides the common functionality for creating PDF letter for
5 * one or a group of contact ids.
7 class CRM_Contribute_Form_Task_PDFLetterCommon
extends CRM_Contact_Form_Task_PDFLetterCommon
{
10 * Build the form object.
12 * @var CRM_Core_Form $form
14 public static function buildQuickForm(&$form) {
15 // use contact form as a base
16 CRM_Contact_Form_Task_PDFLetterCommon
::buildQuickForm($form);
18 // Contribute PDF tasks allow you to email as well, so we need to add email address to those forms
19 $form->add('select', 'from_email_address', ts('From Email Address'), $form->_fromEmails
, TRUE);
20 parent
::buildQuickForm($form);
24 * Process the form after the input has been submitted and validated.
26 * @param CRM_Contribute_Form_Task $form
27 * @param array $formValues
29 public static function postProcess(&$form, $formValues = NULL) {
30 if (empty($formValues)) {
31 $formValues = $form->controller
->exportValues($form->getName());
33 list($formValues, $categories, $html_message, $messageToken, $returnProperties) = self
::processMessageTemplate($formValues);
36 if (!empty($formValues['email_options'])) {
37 $returnProperties['email'] = $returnProperties['on_hold'] = $returnProperties['is_deceased'] = $returnProperties['do_not_email'] = 1;
39 'subject' => CRM_Utils_Array
::value('subject', $formValues),
40 'from' => CRM_Utils_Array
::value('from_email_address', $formValues),
43 $emailParams['from'] = CRM_Utils_Mail
::formatFromAddress($emailParams['from']);
45 // We need display_name for emailLetter() so add to returnProperties here
46 $returnProperties['display_name'] = 1;
47 if (stristr($formValues['email_options'], 'pdfemail')) {
52 $receipt_update = isset($formValues['receipt_update']) ?
$formValues['receipt_update'] : FALSE;
53 $thankyou_update = isset($formValues['thankyou_update']) ?
$formValues['thankyou_update'] : FALSE;
54 $nowDate = date('YmdHis');
55 $receipts = $thanks = $emailed = 0;
57 $task = 'CRM_Contribution_Form_Task_PDFLetterCommon';
58 $realSeparator = ', ';
61 'tr' => '</td></tr><tr><td>',
63 //the original thinking was mutliple options - but we are going with only 2 (comma & td) for now in case
64 // there are security (& UI) issues we need to think through
65 if (isset($formValues['group_by_separator'])) {
66 if (in_array($formValues['group_by_separator'], ['td', 'tr'])) {
67 $realSeparator = $tableSeparators[$formValues['group_by_separator']];
69 elseif ($formValues['group_by_separator'] == 'br') {
70 $realSeparator = "<br />";
73 // a placeholder in case the separator is common in the string - e.g ', '
74 $separator = '****~~~~';
75 $groupBy = $formValues['group_by'];
77 // skip some contacts ?
78 $skipOnHold = isset($form->skipOnHold
) ?
$form->skipOnHold
: FALSE;
79 $skipDeceased = isset($form->skipDeceased
) ?
$form->skipDeceased
: TRUE;
80 $contributionIDs = $form->getVar('_contributionIds');
81 if ($form->_includesSoftCredits
) {
82 //@todo - comment on what is stored there
83 $contributionIDs = $form->getVar('_contributionContactIds');
85 list($contributions, $contacts) = self
::buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $form->_includesSoftCredits
);
87 $contactHtml = $emailedHtml = [];
88 foreach ($contributions as $contributionId => $contribution) {
89 $contact = &$contacts[$contribution['contact_id']];
93 $groupByID = empty($contribution[$groupBy]) ?
0 : $contribution[$groupBy];
94 $contribution = $contact['combined'][$groupBy][$groupByID];
98 if (empty($groupBy) ||
empty($contact['is_sent'][$groupBy][$groupByID])) {
99 $html[$contributionId] = self
::generateHtml($contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID);
100 $contactHtml[$contact['contact_id']][] = $html[$contributionId];
101 if (!empty($formValues['email_options'])) {
102 if (self
::emailLetter($contact, $html[$contributionId], $isPDF, $formValues, $emailParams)) {
104 if (!stristr($formValues['email_options'], 'both')) {
105 $emailedHtml[$contributionId] = TRUE;
109 $contact['is_sent'][$groupBy][$groupByID] = TRUE;
111 // Update receipt/thankyou dates
112 $contributionParams = ['id' => $contributionId];
113 if ($receipt_update) {
114 $contributionParams['receipt_date'] = $nowDate;
116 if ($thankyou_update) {
117 $contributionParams['thankyou_date'] = $nowDate;
119 if ($receipt_update ||
$thankyou_update) {
120 civicrm_api3('Contribution', 'create', $contributionParams);
121 $receipts = ($receipt_update ?
$receipts +
1 : $receipts);
122 $thanks = ($thankyou_update ?
$thanks +
1 : $thanks);
126 $contactIds = array_keys($contacts);
127 self
::createActivities($form, $html_message, $contactIds, CRM_Utils_Array
::value('subject', $formValues, ts('Thank you letter')), CRM_Utils_Array
::value('campaign_id', $formValues), $contactHtml);
128 $html = array_diff_key($html, $emailedHtml);
130 if (!empty($formValues['is_unit_test'])) {
136 $type = $formValues['document_type'];
138 if ($type == 'pdf') {
139 CRM_Utils_PDF_Utils
::html2pdf($html, "CiviLetter.pdf", FALSE, $formValues);
142 CRM_Utils_PDF_Document
::html2doc($html, "CiviLetter.$type", $formValues);
146 $form->postProcessHook();
149 $updateStatus = ts('Receipts have been emailed to %1 contributions.', [1 => $emailed]);
152 $updateStatus = ts('Receipt date has been updated for %1 contributions.', [1 => $receipts]);
155 $updateStatus .= ' ' . ts('Thank-you date has been updated for %1 contributions.', [1 => $thanks]);
159 CRM_Core_Session
::setStatus($updateStatus);
162 // ie. we have only sent emails - lets no show a white screen
163 CRM_Utils_System
::civiExit();
168 * Check whether any of the tokens exist in the html outside a table cell.
169 * If they do the table cell separator is not supported (return false)
170 * At this stage we are only anticipating contributions passed in this way but
171 * it would be easy to add others
177 public static function isValidHTMLWithTableSeparator($tokens, $html) {
178 $relevantEntities = ['contribution'];
179 foreach ($relevantEntities as $entity) {
180 if (isset($tokens[$entity]) && is_array($tokens[$entity])) {
181 foreach ($tokens[$entity] as $token) {
182 if (!self
::isHtmlTokenInTableCell($token, $entity, $html)) {
192 * Check that the token only appears in a table cell. The '</td><td>' separator cannot otherwise work
193 * Calculate the number of times it appears IN the cell & the number of times it appears - should be the same!
195 * @param string $token
196 * @param string $entity
197 * @param string $textToSearch
201 public static function isHtmlTokenInTableCell($token, $entity, $textToSearch) {
202 $tokenToMatch = $entity . '\.' . $token;
203 $pattern = '|<td(?![\w-])((?!</td>).)*\{' . $tokenToMatch . '\}.*?</td>|si';
204 $within = preg_match_all($pattern, $textToSearch);
205 $total = preg_match_all("|{" . $tokenToMatch . "}|", $textToSearch);
206 return ($within == $total);
211 * @param string $html_message
212 * @param array $contact
213 * @param array $contribution
214 * @param array $messageToken
215 * @param bool $grouped
216 * Does this letter represent more than one contribution.
217 * @param string $separator
218 * What is the preferred letter separator.
221 private static function resolveTokens($html_message, $contact, $contribution, $messageToken, $grouped, $separator) {
222 $categories = self
::getTokenCategories();
223 $tokenHtml = CRM_Utils_Token
::replaceContactTokens($html_message, $contact, TRUE, $messageToken);
225 $tokenHtml = CRM_Utils_Token
::replaceMultipleContributionTokens($separator, $tokenHtml, $contribution, TRUE, $messageToken);
228 // no change to normal behaviour to avoid risk of breakage
229 $tokenHtml = CRM_Utils_Token
::replaceContributionTokens($tokenHtml, $contribution, TRUE, $messageToken);
231 $tokenHtml = CRM_Utils_Token
::replaceHookTokens($tokenHtml, $contact, $categories, TRUE);
232 if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY
) {
233 $smarty = CRM_Core_Smarty
::singleton();
234 // also add the tokens to the template
235 $smarty->assign_by_ref('contact', $contact);
236 $tokenHtml = $smarty->fetch("string:$tokenHtml");
242 * Generate the contribution array from the form, we fill in the contact details and determine any aggregation
243 * around contact_id of contribution_recur_id
245 * @param string $groupBy
246 * @param array $contributionIDs
247 * @param array $returnProperties
248 * @param bool $skipOnHold
249 * @param bool $skipDeceased
250 * @param array $messageToken
251 * @param string $task
252 * @param string $separator
253 * @param bool $isIncludeSoftCredits
257 public static function buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $isIncludeSoftCredits) {
258 $contributions = $contacts = [];
259 foreach ($contributionIDs as $item => $contributionId) {
260 $contribution = CRM_Contribute_BAO_Contribution
::getContributionTokenValues($contributionId, $messageToken)['values'][$contributionId];
261 $contribution['campaign'] = CRM_Utils_Array
::value('contribution_campaign_title', $contribution);
262 $contributions[$contributionId] = $contribution;
264 if ($isIncludeSoftCredits) {
265 //@todo find out why this happens & add comments
266 list($contactID) = explode('-', $item);
267 $contactID = (int) $contactID;
270 $contactID = $contribution['contact_id'];
272 if (!isset($contacts[$contactID])) {
273 $contacts[$contactID] = [];
274 $contacts[$contactID]['contact_aggregate'] = 0;
275 $contacts[$contactID]['combined'] = $contacts[$contactID]['contribution_ids'] = [];
278 $contacts[$contactID]['contact_aggregate'] +
= $contribution['total_amount'];
279 $groupByID = empty($contribution[$groupBy]) ?
0 : $contribution[$groupBy];
281 $contacts[$contactID]['contribution_ids'][$groupBy][$groupByID][$contributionId] = TRUE;
282 if (!isset($contacts[$contactID]['combined'][$groupBy]) ||
!isset($contacts[$contactID]['combined'][$groupBy][$groupByID])) {
283 $contacts[$contactID]['combined'][$groupBy][$groupByID] = $contribution;
284 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] = $contribution['total_amount'];
287 $contacts[$contactID]['combined'][$groupBy][$groupByID] = self
::combineContributions($contacts[$contactID]['combined'][$groupBy][$groupByID], $contribution, $separator);
288 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] +
= $contribution['total_amount'];
291 // Assign the available contributions before calling tokens so hooks parsing smarty can access it.
292 // Note that in core code you can only use smarty here if enable if for the whole site, incl
293 // CiviMail, with a big performance impact.
294 // Hooks allow more nuanced smarty usage here.
295 CRM_Core_Smarty
::singleton()->assign('contributions', $contributions);
296 foreach ($contacts as $contactID => $contact) {
297 $tokenResolvedContacts = CRM_Utils_Token
::getTokenDetails(['contact_id' => $contactID],
305 $contacts[$contactID] = array_merge($tokenResolvedContacts[0][$contactID], $contact);
307 return [$contributions, $contacts];
311 * We combine the contributions by adding the contribution to each field with the separator in
312 * between the existing value and the new one. We put the separator there even if empty so it is clear what the
313 * value for previous contributions was
315 * @param array $existing
316 * @param array $contribution
317 * @param string $separator
321 public static function combineContributions($existing, $contribution, $separator) {
322 foreach ($contribution as $field => $value) {
323 $existing[$field] = isset($existing[$field]) ?
$existing[$field] . $separator : '';
324 $existing[$field] .= $value;
330 * We are going to retrieve the combined contribution and if smarty mail is enabled we
331 * will also assign an array of contributions for this contact to the smarty template
333 * @param array $contact
334 * @param array $contributions
336 * @param int $groupByID
338 public static function assignCombinedContributionValues($contact, $contributions, $groupBy, $groupByID) {
339 CRM_Core_Smarty
::singleton()->assign('contact_aggregate', $contact['contact_aggregate']);
340 CRM_Core_Smarty
::singleton()
341 ->assign('contributions', array_intersect_key($contributions, $contact['contribution_ids'][$groupBy][$groupByID]));
342 CRM_Core_Smarty
::singleton()->assign('contribution_aggregate', $contact['aggregates'][$groupBy][$groupByID]);
349 * @param array $contact
350 * @param string $html
353 * @param array $format
354 * @param array $params
358 public static function emailLetter($contact, $html, $is_pdf, $format = [], $params = []) {
360 if (empty($contact['email'])) {
363 $mustBeEmpty = ['do_not_email', 'is_deceased', 'on_hold'];
364 foreach ($mustBeEmpty as $emptyField) {
365 if (!empty($contact[$emptyField])) {
371 'toName' => $contact['display_name'],
372 'toEmail' => $contact['email'],
376 if (empty($params['from'])) {
377 $emails = CRM_Core_BAO_Email
::getFromEmail();
378 $emails = array_keys($emails);
379 $defaults['from'] = array_pop($emails);
382 $defaults['from'] = $params['from'];
384 if (!empty($params['subject'])) {
385 $defaults['subject'] = $params['subject'];
388 $defaults['subject'] = ts('Thank you for your contribution/s');
391 $defaults['html'] = ts('Please see attached');
392 $defaults['attachments'] = [CRM_Utils_Mail
::appendPDF('ThankYou.pdf', $html, $format)];
394 $params = array_merge($defaults);
395 return CRM_Utils_Mail
::send($params);
397 catch (CRM_Core_Exception
$e) {
405 * @param $contribution
407 * @param $contributions
408 * @param $realSeparator
409 * @param $tableSeparators
410 * @param $messageToken
411 * @param $html_message
414 * @param bool $grouped
415 * @param int $groupByID
419 protected static function generateHtml(&$contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID) {
420 static $validated = FALSE;
423 self
::assignCombinedContributionValues($contact, $contributions, $groupBy, $groupByID);
425 if (empty($groupBy) ||
empty($contact['is_sent'][$groupBy][$groupByID])) {
426 if (!$validated && in_array($realSeparator, $tableSeparators) && !self
::isValidHTMLWithTableSeparator($messageToken, $html_message)) {
427 $realSeparator = ', ';
428 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.'));
431 $html = str_replace($separator, $realSeparator, self
::resolveTokens($html_message, $contact, $contribution, $messageToken, $grouped, $separator));