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