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