Merge pull request #13967 from eileenmcnaughton/activity_token
[civicrm-core.git] / CRM / Contribute / Form / Task / PDFLetterCommon.php
CommitLineData
6a488035
TO
1<?php
2
3/**
4 * This class provides the common functionality for creating PDF letter for
5 * one or a group of contact ids.
6 */
7class CRM_Contribute_Form_Task_PDFLetterCommon extends CRM_Contact_Form_Task_PDFLetterCommon {
8
2cac4853
MW
9 /**
10 * Build the form object.
11 *
12 * @var CRM_Core_Form $form
13 */
14 public static function buildQuickForm(&$form) {
99ad38df
MW
15 // use contact form as a base
16 CRM_Contact_Form_Task_PDFLetterCommon::buildQuickForm($form);
17
2cac4853
MW
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);
21 }
22
6a488035 23 /**
fe482240 24 * Process the form after the input has been submitted and validated.
6a488035 25 *
493078cb 26 * @param CRM_Contribute_Form_Task $form
59d861cb 27 * @param array $formValues
6a488035 28 */
59d861cb 29 public static function postProcess(&$form, $formValues = NULL) {
30 if (empty($formValues)) {
31 $formValues = $form->controller->exportValues($form->getName());
32 }
b0738a9b 33 list($formValues, $categories, $html_message, $messageToken, $returnProperties) = self::processMessageTemplate($formValues);
493078cb
EM
34 $isPDF = FALSE;
35 $emailParams = array();
22e263ad 36 if (!empty($formValues['email_options'])) {
383c047b
DG
37 $returnProperties['email'] = $returnProperties['on_hold'] = $returnProperties['is_deceased'] = $returnProperties['do_not_email'] = 1;
38 $emailParams = array(
2cac4853
MW
39 'subject' => CRM_Utils_Array::value('subject', $formValues),
40 'from' => CRM_Utils_Array::value('from_email_address', $formValues),
383c047b 41 );
80b646c0
ML
42
43 $emailParams['from'] = CRM_Utils_Mail::formatFromAddress($emailParams['from']);
44
f83f6352
DG
45 // We need display_name for emailLetter() so add to returnProperties here
46 $returnProperties['display_name'] = 1;
22e263ad 47 if (stristr($formValues['email_options'], 'pdfemail')) {
383c047b
DG
48 $isPDF = TRUE;
49 }
50 }
6a488035 51 // update dates ?
353ffa53 52 $receipt_update = isset($formValues['receipt_update']) ? $formValues['receipt_update'] : FALSE;
6a488035 53 $thankyou_update = isset($formValues['thankyou_update']) ? $formValues['thankyou_update'] : FALSE;
353ffa53 54 $nowDate = date('YmdHis');
383c047b 55 $receipts = $thanks = $emailed = 0;
353ffa53 56 $updateStatus = '';
383c047b
DG
57 $task = 'CRM_Contribution_Form_Task_PDFLetterCommon';
58 $realSeparator = ', ';
b26a113f 59 $tableSeparators = array(
60 'td' => '</td><td>',
61 'tr' => '</td></tr><tr><td>',
62 );
383c047b
DG
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
22e263ad 65 if (isset($formValues['group_by_separator'])) {
b26a113f 66 if (in_array($formValues['group_by_separator'], array('td', 'tr'))) {
67 $realSeparator = $tableSeparators[$formValues['group_by_separator']];
6edcec04
YR
68 }
69 elseif ($formValues['group_by_separator'] == 'br') {
70 $realSeparator = "<br />";
71 }
72 }
383c047b 73 $separator = '****~~~~';// a placeholder in case the separator is common in the string - e.g ', '
383c047b 74 $groupBy = $formValues['group_by'];
6a488035
TO
75
76 // skip some contacts ?
77 $skipOnHold = isset($form->skipOnHold) ? $form->skipOnHold : FALSE;
78 $skipDeceased = isset($form->skipDeceased) ? $form->skipDeceased : TRUE;
e9379b58 79 $contributionIDs = $form->getVar('_contributionIds');
80 if ($form->_includesSoftCredits) {
81 //@todo - comment on what is stored there
82 $contributionIDs = $form->getVar('_contributionContactIds');
83 }
84 list($contributions, $contacts) = self::buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $form->_includesSoftCredits);
383c047b 85 $html = array();
3280f327 86 $contactHtml = $emailedHtml = array();
383c047b
DG
87 foreach ($contributions as $contributionId => $contribution) {
88 $contact = &$contacts[$contribution['contact_id']];
2fe8b920 89 $grouped = FALSE;
90 $groupByID = 0;
22e263ad 91 if ($groupBy) {
383c047b
DG
92 $groupByID = empty($contribution[$groupBy]) ? 0 : $contribution[$groupBy];
93 $contribution = $contact['combined'][$groupBy][$groupByID];
94 $grouped = TRUE;
6a488035
TO
95 }
96
22e263ad 97 if (empty($groupBy) || empty($contact['is_sent'][$groupBy][$groupByID])) {
2fe8b920 98 $html[$contributionId] = self::generateHtml($contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID);
99 $contactHtml[$contact['contact_id']][] = $html[$contributionId];
22e263ad
TO
100 if (!empty($formValues['email_options'])) {
101 if (self::emailLetter($contact, $html[$contributionId], $isPDF, $formValues, $emailParams)) {
d3e86119 102 $emailed++;
22e263ad 103 if (!stristr($formValues['email_options'], 'both')) {
2fe8b920 104 $emailedHtml[$contributionId] = TRUE;
383c047b
DG
105 }
106 }
107 }
2fe8b920 108 $contact['is_sent'][$groupBy][$groupByID] = TRUE;
6a488035 109 }
fd578207
MW
110 // Update receipt/thankyou dates
111 $contributionParams = array('id' => $contributionId);
6a488035 112 if ($receipt_update) {
fd578207 113 $contributionParams['receipt_date'] = $nowDate;
6a488035
TO
114 }
115 if ($thankyou_update) {
fd578207
MW
116 $contributionParams['thankyou_date'] = $nowDate;
117 }
118 if ($receipt_update || $thankyou_update) {
119 civicrm_api3('Contribution', 'create', $contributionParams);
120 $receipts = ($receipt_update ? $receipts + 1 : $receipts);
121 $thanks = ($thankyou_update ? $thanks + 1 : $thanks);
6a488035
TO
122 }
123 }
59d861cb 124
fe61faf3 125 $contactIds = array_keys($contacts);
3280f327 126 self::createActivities($form, $html_message, $contactIds, CRM_Utils_Array::value('subject', $formValues, ts('Thank you letter')), CRM_Utils_Array::value('campaign_id', $formValues), $contactHtml);
127 $html = array_diff_key($html, $emailedHtml);
2fe8b920 128
59d861cb 129 if (!empty($formValues['is_unit_test'])) {
130 return $html;
131 }
2fe8b920 132
0f2a9bd2 133 //CRM-19761
22e263ad 134 if (!empty($html)) {
0f2a9bd2
BS
135 $type = $formValues['document_type'];
136
137 if ($type == 'pdf') {
138 CRM_Utils_PDF_Utils::html2pdf($html, "CiviLetter.pdf", FALSE, $formValues);
139 }
140 else {
141 CRM_Utils_PDF_Document::html2doc($html, "CiviLetter.$type", $formValues);
142 }
383c047b 143 }
6a488035
TO
144
145 $form->postProcessHook();
146
383c047b
DG
147 if ($emailed) {
148 $updateStatus = ts('Receipts have been emailed to %1 contributions.', array(1 => $emailed));
149 }
6a488035
TO
150 if ($receipts) {
151 $updateStatus = ts('Receipt date has been updated for %1 contributions.', array(1 => $receipts));
152 }
153 if ($thanks) {
154 $updateStatus .= ' ' . ts('Thank-you date has been updated for %1 contributions.', array(1 => $thanks));
155 }
156
157 if ($updateStatus) {
158 CRM_Core_Session::setStatus($updateStatus);
159 }
22e263ad 160 if (!empty($html)) {
383c047b 161 // ie. we have only sent emails - lets no show a white screen
292c8687 162 CRM_Utils_System::civiExit();
383c047b
DG
163 }
164 }
665e5ec7 165
8cb33f3b
EM
166 /**
167 * Check whether any of the tokens exist in the html outside a table cell.
168 * If they do the table cell separator is not supported (return false)
169 * At this stage we are only anticipating contributions passed in this way but
170 * it would be easy to add others
171 * @param $tokens
172 * @param $html
173 *
174 * @return bool
175 */
00be9182 176 public static function isValidHTMLWithTableSeparator($tokens, $html) {
8cb33f3b
EM
177 $relevantEntities = array('contribution');
178 foreach ($relevantEntities as $entity) {
045bdb93 179 if (isset($tokens[$entity]) && is_array($tokens[$entity])) {
8cb33f3b 180 foreach ($tokens[$entity] as $token) {
353ffa53 181 if (!self::isHtmlTokenInTableCell($token, $entity, $html)) {
8cb33f3b
EM
182 return FALSE;
183 }
184 }
185 }
186 }
187 return TRUE;
188 }
189
190 /**
100fef9d 191 * Check that the token only appears in a table cell. The '</td><td>' separator cannot otherwise work
045bdb93
EM
192 * Calculate the number of times it appears IN the cell & the number of times it appears - should be the same!
193 *
8dfeb798
SM
194 * @param string $token
195 * @param string $entity
196 * @param string $textToSearch
045bdb93 197 *
045bdb93 198 * @return bool
8cb33f3b 199 */
00be9182 200 public static function isHtmlTokenInTableCell($token, $entity, $textToSearch) {
5aece37d
SM
201 $tokenToMatch = $entity . '\.' . $token;
202 $pattern = '|<td(?![\w-])((?!</td>).)*\{' . $tokenToMatch . '\}.*?</td>|si';
8dfeb798
SM
203 $within = preg_match_all($pattern, $textToSearch);
204 $total = preg_match_all("|{" . $tokenToMatch . "}|", $textToSearch);
045bdb93 205 return ($within == $total);
8cb33f3b
EM
206 }
207
874c9be7
TO
208 /**
209 *
210 * @param string $html_message
211 * @param array $contact
212 * @param array $contribution
213 * @param array $messageToken
874c9be7
TO
214 * @param bool $grouped
215 * Does this letter represent more than one contribution.
216 * @param string $separator
217 * What is the preferred letter separator.
218 * @return string
219 */
3280f327 220 private static function resolveTokens($html_message, $contact, $contribution, $messageToken, $grouped, $separator) {
221 $categories = self::getTokenCategories();
874c9be7 222 $tokenHtml = CRM_Utils_Token::replaceContactTokens($html_message, $contact, TRUE, $messageToken);
22e263ad 223 if ($grouped) {
874c9be7
TO
224 $tokenHtml = CRM_Utils_Token::replaceMultipleContributionTokens($separator, $tokenHtml, $contribution, TRUE, $messageToken);
225 }
226 else {
227 // no change to normal behaviour to avoid risk of breakage
228 $tokenHtml = CRM_Utils_Token::replaceContributionTokens($tokenHtml, $contribution, TRUE, $messageToken);
229 }
230 $tokenHtml = CRM_Utils_Token::replaceHookTokens($tokenHtml, $contact, $categories, TRUE);
231 if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY) {
383c047b
DG
232 $smarty = CRM_Core_Smarty::singleton();
233 // also add the tokens to the template
234 $smarty->assign_by_ref('contact', $contact);
235 $tokenHtml = $smarty->fetch("string:$tokenHtml");
236 }
237 return $tokenHtml;
238 }
239
240 /**
241 * Generate the contribution array from the form, we fill in the contact details and determine any aggregation
242 * around contact_id of contribution_recur_id
243 *
045bdb93 244 * @param string $groupBy
e9379b58 245 * @param array $contributionIDs
354bc6e1 246 * @param array $returnProperties
014c4014
TO
247 * @param bool $skipOnHold
248 * @param bool $skipDeceased
354bc6e1 249 * @param array $messageToken
8cb33f3b 250 * @param string $task
045bdb93 251 * @param string $separator
e9379b58 252 * @param bool $isIncludeSoftCredits
fe7febc7 253 *
a6c01b45 254 * @return array
383c047b 255 */
e9379b58 256 public static function buildContributionArray($groupBy, $contributionIDs, $returnProperties, $skipOnHold, $skipDeceased, $messageToken, $task, $separator, $isIncludeSoftCredits) {
3280f327 257 $contributions = $contacts = array();
383c047b 258 foreach ($contributionIDs as $item => $contributionId) {
3280f327 259 // Basic return attributes available to the template.
260 $returnValues = array('contact_id', 'total_amount', 'financial_type', 'receive_date', 'contribution_campaign_title');
39b23159
ERL
261 if (!empty($messageToken['contribution'])) {
262 $returnValues = array_merge($messageToken['contribution'], $returnValues);
263 }
264 // retrieve contribution tokens listed in $returnProperties using Contribution.Get API
265 $contribution = civicrm_api3('Contribution', 'getsingle', array(
266 'id' => $contributionId,
267 'return' => $returnValues,
268 ));
d31d0888 269 $contribution['campaign'] = CRM_Utils_Array::value('contribution_campaign_title', $contribution);
39b23159 270 $contributions[$contributionId] = $contribution;
8ea4b051 271
e9379b58 272 if ($isIncludeSoftCredits) {
ff926d67
EM
273 //@todo find out why this happens & add comments
274 list($contactID) = explode('-', $item);
275 $contactID = (int) $contactID;
874c9be7 276 }
ff926d67 277 else {
874c9be7 278 $contactID = $contribution['contact_id'];
ff926d67 279 }
22e263ad 280 if (!isset($contacts[$contactID])) {
8ea4b051 281 $contacts[$contactID] = array();
ff926d67
EM
282 $contacts[$contactID]['contact_aggregate'] = 0;
283 $contacts[$contactID]['combined'] = $contacts[$contactID]['contribution_ids'] = array();
284 }
383c047b 285
ff926d67
EM
286 $contacts[$contactID]['contact_aggregate'] += $contribution['total_amount'];
287 $groupByID = empty($contribution[$groupBy]) ? 0 : $contribution[$groupBy];
383c047b 288
ff926d67 289 $contacts[$contactID]['contribution_ids'][$groupBy][$groupByID][$contributionId] = TRUE;
22e263ad 290 if (!isset($contacts[$contactID]['combined'][$groupBy]) || !isset($contacts[$contactID]['combined'][$groupBy][$groupByID])) {
ff926d67
EM
291 $contacts[$contactID]['combined'][$groupBy][$groupByID] = $contribution;
292 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] = $contribution['total_amount'];
383c047b 293 }
ff926d67
EM
294 else {
295 $contacts[$contactID]['combined'][$groupBy][$groupByID] = self::combineContributions($contacts[$contactID]['combined'][$groupBy][$groupByID], $contribution, $separator);
296 $contacts[$contactID]['aggregates'][$groupBy][$groupByID] += $contribution['total_amount'];
383c047b
DG
297 }
298 }
8ea4b051 299 // Assign the available contributions before calling tokens so hooks parsing smarty can access it.
300 // Note that in core code you can only use smarty here if enable if for the whole site, incl
301 // CiviMail, with a big performance impact.
302 // Hooks allow more nuanced smarty usage here.
303 CRM_Core_Smarty::singleton()->assign('contributions', $contributions);
304 foreach ($contacts as $contactID => $contact) {
305 $tokenResolvedContacts = CRM_Utils_Token::getTokenDetails(array('contact_id' => $contactID),
306 $returnProperties,
307 $skipOnHold,
308 $skipDeceased,
309 NULL,
310 $messageToken,
311 $task
312 );
313 $contacts[$contactID] = array_merge($tokenResolvedContacts[0][$contactID], $contact);
314 }
ff926d67 315 return array($contributions, $contacts);
383c047b
DG
316 }
317
318 /**
319 * We combine the contributions by adding the contribution to each field with the separator in
320 * between the existing value and the new one. We put the separator there even if empty so it is clear what the
321 * value for previous contributions was
fe7febc7 322 *
045bdb93
EM
323 * @param array $existing
324 * @param array $contribution
325 * @param string $separator
fe7febc7 326 *
045bdb93 327 * @return array
383c047b 328 */
00be9182 329 public static function combineContributions($existing, $contribution, $separator) {
383c047b 330 foreach ($contribution as $field => $value) {
7eebfcb9 331 $existing[$field] = isset($existing[$field]) ? $existing[$field] . $separator : '';
353ffa53 332 $existing[$field] .= $value;
383c047b
DG
333 }
334 return $existing;
335 }
336
337 /**
338 * We are going to retrieve the combined contribution and if smarty mail is enabled we
339 * will also assign an array of contributions for this contact to the smarty template
fe7febc7 340 *
383c047b
DG
341 * @param array $contact
342 * @param array $contributions
fe7febc7 343 * @param $groupBy
100fef9d 344 * @param int $groupByID
383c047b 345 */
00be9182 346 public static function assignCombinedContributionValues($contact, $contributions, $groupBy, $groupByID) {
383c047b 347 CRM_Core_Smarty::singleton()->assign('contact_aggregate', $contact['contact_aggregate']);
353ffa53
TO
348 CRM_Core_Smarty::singleton()
349 ->assign('contributions', array_intersect_key($contributions, $contact['contribution_ids'][$groupBy][$groupByID]));
383c047b
DG
350 CRM_Core_Smarty::singleton()->assign('contribution_aggregate', $contact['aggregates'][$groupBy][$groupByID]);
351
352 }
353
354 /**
fe482240 355 * Send pdf by email.
fe7febc7 356 *
383c047b
DG
357 * @param array $contact
358 * @param string $html
fe7febc7
EM
359 *
360 * @param $is_pdf
361 * @param array $format
362 * @param array $params
363 *
364 * @return bool
383c047b 365 */
00be9182 366 public static function emailLetter($contact, $html, $is_pdf, $format = array(), $params = array()) {
383c047b 367 try {
22e263ad 368 if (empty($contact['email'])) {
383c047b
DG
369 return FALSE;
370 }
371 $mustBeEmpty = array('do_not_email', 'is_deceased', 'on_hold');
372 foreach ($mustBeEmpty as $emptyField) {
22e263ad 373 if (!empty($contact[$emptyField])) {
383c047b
DG
374 return FALSE;
375 }
376 }
377
378 $defaults = array(
379 'toName' => $contact['display_name'],
380 'toEmail' => $contact['email'],
383c047b
DG
381 'text' => '',
382 'html' => $html,
383 );
22e263ad 384 if (empty($params['from'])) {
383c047b
DG
385 $emails = CRM_Core_BAO_Email::getFromEmail();
386 $emails = array_keys($emails);
387 $defaults['from'] = array_pop($emails);
388 }
beac1417
MW
389 else {
390 $defaults['from'] = $params['from'];
391 }
ab0f580c 392 if (!empty($params['subject'])) {
c285f292 393 $defaults['subject'] = $params['subject'];
ab0f580c
BM
394 }
395 else {
c285f292 396 $defaults['subject'] = ts('Thank you for your contribution/s');
ab0f580c 397 }
22e263ad 398 if ($is_pdf) {
383c047b
DG
399 $defaults['html'] = ts('Please see attached');
400 $defaults['attachments'] = array(CRM_Utils_Mail::appendPDF('ThankYou.pdf', $html, $format));
401 }
402 $params = array_merge($defaults);
403 return CRM_Utils_Mail::send($params);
404 }
405 catch (CRM_Core_Exception $e) {
406 return FALSE;
407 }
6a488035 408 }
96025800 409
2fe8b920 410 /**
411 * @param $contact
412 * @param $formValues
413 * @param $contribution
414 * @param $groupBy
415 * @param $contributions
416 * @param $realSeparator
417 * @param $tableSeparators
418 * @param $messageToken
419 * @param $html_message
420 * @param $separator
421 * @param $categories
422 * @param bool $grouped
423 * @param int $groupByID
424 *
425 * @return string
426 */
427 protected static function generateHtml(&$contact, $contribution, $groupBy, $contributions, $realSeparator, $tableSeparators, $messageToken, $html_message, $separator, $grouped, $groupByID) {
428 static $validated = FALSE;
429 $html = NULL;
430
2fe8b920 431 self::assignCombinedContributionValues($contact, $contributions, $groupBy, $groupByID);
432
433 if (empty($groupBy) || empty($contact['is_sent'][$groupBy][$groupByID])) {
434 if (!$validated && in_array($realSeparator, $tableSeparators) && !self::isValidHTMLWithTableSeparator($messageToken, $html_message)) {
435 $realSeparator = ', ';
436 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.'));
437 }
438 $validated = TRUE;
3280f327 439 $html = str_replace($separator, $realSeparator, self::resolveTokens($html_message, $contact, $contribution, $messageToken, $grouped, $separator));
2fe8b920 440 }
441
442 return $html;
443 }
444
6a488035 445}