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