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