Merge pull request #21490 from eileenmcnaughton/case_email
[civicrm-core.git] / CRM / Contact / Form / Task / PDFTrait.php
CommitLineData
fc34a273
EM
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12/**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18/**
19 * This class provides the common functionality for tasks that send emails.
20 */
21trait CRM_Contact_Form_Task_PDFTrait {
22
23 /**
24 * Set defaults for the pdf.
25 *
26 * @return array
27 */
28 public function setDefaultValues(): array {
29 return $this->getPDFDefaultValues();
30 }
31
32 /**
33 * Set default values.
34 */
35 protected function getPDFDefaultValues(): array {
36 $defaultFormat = CRM_Core_BAO_PdfFormat::getDefaultValues();
37 $defaultFormat['format_id'] = $defaultFormat['id'];
38 return $defaultFormat;
39 }
40
053c1a4b
EM
41 /**
42 * Build the form object.
43 *
44 * @throws \CRM_Core_Exception
45 */
46 public function buildQuickForm(): void {
47 $this->addPDFElementsToForm();
48 }
49
50 /**
51 * Build the form object.
52 *
53 * @throws \CRM_Core_Exception
54 */
55 public function addPDFElementsToForm(): void {
56 $form = $this;
57 // This form outputs a file so should never be submitted via ajax
58 $form->preventAjaxSubmit();
59
60 //Added for CRM-12682: Add activity subject and campaign fields
61 CRM_Campaign_BAO_Campaign::addCampaign($form);
62 $form->add(
63 'text',
64 'subject',
65 ts('Activity Subject'),
66 ['size' => 45, 'maxlength' => 255],
67 FALSE
68 );
69
70 // Added for core#2121,
71 // To support sending a custom pdf filename before downloading.
72 $form->addElement('hidden', 'pdf_file_name');
73
74 $form->addSelect('format_id', [
75 'label' => ts('Select Format'),
76 'placeholder' => ts('Default'),
77 'entity' => 'message_template',
78 'field' => 'pdf_format_id',
79 'option_url' => 'civicrm/admin/pdfFormats',
80 ]);
81 $form->add(
82 'select',
83 'paper_size',
84 ts('Paper Size'),
85 [0 => ts('- default -')] + CRM_Core_BAO_PaperSize::getList(TRUE),
86 FALSE,
87 ['onChange' => "selectPaper( this.value ); showUpdateFormatChkBox();"]
88 );
89 $form->add(
90 'select',
91 'orientation',
92 ts('Orientation'),
93 CRM_Core_BAO_PdfFormat::getPageOrientations(),
94 FALSE,
95 ['onChange' => "updatePaperDimensions(); showUpdateFormatChkBox();"]
96 );
97 $form->add(
98 'select',
99 'metric',
100 ts('Unit of Measure'),
101 CRM_Core_BAO_PdfFormat::getUnits(),
102 FALSE,
103 ['onChange' => "selectMetric( this.value );"]
104 );
105 $form->add(
106 'text',
107 'margin_left',
108 ts('Left Margin'),
109 ['size' => 8, 'maxlength' => 8, 'onkeyup' => "showUpdateFormatChkBox();"],
110 TRUE
111 );
112 $form->add(
113 'text',
114 'margin_right',
115 ts('Right Margin'),
116 ['size' => 8, 'maxlength' => 8, 'onkeyup' => "showUpdateFormatChkBox();"],
117 TRUE
118 );
119 $form->add(
120 'text',
121 'margin_top',
122 ts('Top Margin'),
123 ['size' => 8, 'maxlength' => 8, 'onkeyup' => "showUpdateFormatChkBox();"],
124 TRUE
125 );
126 $form->add(
127 'text',
128 'margin_bottom',
129 ts('Bottom Margin'),
130 ['size' => 8, 'maxlength' => 8, 'onkeyup' => "showUpdateFormatChkBox();"],
131 TRUE
132 );
133
134 $config = CRM_Core_Config::singleton();
135 /** CRM-15883 Suppressing Stationery path field until we switch from DOMPDF to a library that supports it.
136 * if ($config->wkhtmltopdfPath == FALSE) {
137 * $form->add(
138 * 'text',
139 * 'stationery',
140 * ts('Stationery (relative path to PDF you wish to use as the background)'),
141 * array('size' => 25, 'maxlength' => 900, 'onkeyup' => "showUpdateFormatChkBox();"),
142 * FALSE
143 * );
144 * }
145 */
146 $form->add('checkbox', 'bind_format', ts('Always use this Page Format with the selected Template'));
147 $form->add('checkbox', 'update_format', ts('Update Page Format (this will affect all templates that use this format)'));
148
149 $form->assign('useThisPageFormat', ts('Always use this Page Format with the new template?'));
150 $form->assign('useSelectedPageFormat', ts('Should the new template always use the selected Page Format?'));
151 $form->assign('totalSelectedContacts', !is_null($form->_contactIds) ? count($form->_contactIds) : 0);
152
153 $form->add('select', 'document_type', ts('Document Type'), CRM_Core_SelectValues::documentFormat());
154
155 $documentTypes = implode(',', CRM_Core_SelectValues::documentApplicationType());
156 $form->addElement('file', "document_file", 'Upload Document', 'size=30 maxlength=255 accept="' . $documentTypes . '"');
157 $form->addUploadElement("document_file");
158
159 CRM_Mailing_BAO_Mailing::commonCompose($form);
160
f336046d 161 $buttons = $this->getButtons($form);
053c1a4b
EM
162 $form->addButtons($buttons);
163
164 $form->addFormRule(['CRM_Core_Form_Task_PDFLetterCommon', 'formRule'], $form);
165 }
166
c97bfeff
EM
167 /**
168 * Prepare form.
169 */
170 public function preProcessPDF(): void {
171 $form = $this;
172 $defaults = [];
173 $form->_fromEmails = CRM_Core_BAO_Email::getFromEmail();
174 if (is_numeric(key($form->_fromEmails))) {
175 $emailID = (int) key($form->_fromEmails);
176 $defaults = CRM_Core_BAO_Email::getEmailSignatureDefaults($emailID);
177 }
178 if (!Civi::settings()->get('allow_mail_from_logged_in_contact')) {
179 $defaults['from_email_address'] = current(CRM_Core_BAO_Domain::getNameAndEmail(FALSE, TRUE));
180 }
181 $form->setDefaults($defaults);
182 $form->setTitle('Print/Merge Document');
183 }
184
7c0d6f7a
EM
185 /**
186 * Returns the filename for the pdf by striping off unwanted characters and limits the length to 200 characters.
187 *
188 * @return string
189 * The name of the file.
190 */
191 public function getFileName(): string {
192 if (!empty($this->getSubmittedValue('pdf_file_name'))) {
193 $fileName = CRM_Utils_File::makeFilenameWithUnicode($this->getSubmittedValue('pdf_file_name'), '_', 200);
194 }
195 elseif (!empty($this->getSubmittedValue('subject'))) {
196 $fileName = CRM_Utils_File::makeFilenameWithUnicode($this->getSubmittedValue('subject'), '_', 200);
197 }
198 else {
199 $fileName = 'CiviLetter';
200 }
201 return $this->isLiveMode() ? $fileName : $fileName . '_preview';
202 }
203
204 /**
205 * Is the form in live mode (as opposed to being run as a preview).
206 *
207 * Returns true if the user has clicked the Download Document button on a
208 * Print/Merge Document (PDF Letter) search task form, or false if the Preview
209 * button was clicked.
210 *
211 * @return bool
212 * TRUE if the Download Document button was clicked (also defaults to TRUE
213 * if the form controller does not exist), else FALSE
214 */
215 protected function isLiveMode(): bool {
f336046d 216 return strpos($this->controller->getButtonName(), '_preview') === FALSE;
7c0d6f7a
EM
217 }
218
fb4f4e89
EM
219 /**
220 * Process the form after the input has been submitted and validated.
221 *
222 * @throws \CRM_Core_Exception
223 * @throws \CiviCRM_API3_Exception
224 * @throws \API_Exception
225 */
226 public function postProcess(): void {
227 $form = $this;
228 $formValues = $form->controller->exportValues($form->getName());
0ceb63d9 229 [$formValues, $html_message, $messageToken, $returnProperties] = $this->processMessageTemplate($formValues);
fb4f4e89
EM
230 $html = $activityIds = [];
231
232 // CRM-16725 Skip creation of activities if user is previewing their PDF letter(s)
233 if ($this->isLiveMode()) {
234 $activityIds = $this->createActivities($form, $html_message, $form->_contactIds, $formValues['subject'], CRM_Utils_Array::value('campaign_id', $formValues));
235 }
236
237 if (!empty($formValues['document_file_path'])) {
238 [$html_message, $zip] = CRM_Utils_PDF_Document::unzipDoc($formValues['document_file_path'], $formValues['document_type']);
239 }
240
241 foreach ($form->_contactIds as $item => $contactId) {
242 $caseId = $form->getVar('_caseId');
243 if (empty($caseId) && !empty($form->_caseIds[$item])) {
244 $caseId = $form->_caseIds[$item];
245 }
246
247 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
248 'contactId' => $contactId,
249 'messageTemplate' => ['msg_html' => $html_message],
250 'tokenContext' => $caseId ? ['caseId' => $caseId] : [],
251 'disableSmarty' => (!defined('CIVICRM_MAIL_SMARTY') || !CIVICRM_MAIL_SMARTY),
252 ])['html'];
253
254 $html[] = $tokenHtml;
255 }
256
257 $tee = NULL;
258 if ($this->isLiveMode() && Civi::settings()->get('recordGeneratedLetters') === 'combined-attached') {
259 if (count($activityIds) !== 1) {
260 throw new CRM_Core_Exception("When recordGeneratedLetters=combined-attached, there should only be one activity.");
261 }
262 $tee = CRM_Utils_ConsoleTee::create()->start();
263 }
264
265 $type = $formValues['document_type'];
266 $mimeType = $this->getMimeType($type);
267 // ^^ Useful side-effect: consistently throws error for unrecognized types.
268
269 $fileName = method_exists($form, 'getFileName') ? ($form->getFileName() . '.' . $type) : 'CiviLetter.' . $type;
270
271 if ($type === 'pdf') {
272 CRM_Utils_PDF_Utils::html2pdf($html, $fileName, FALSE, $formValues);
273 }
274 elseif (!empty($formValues['document_file_path'])) {
275 $fileName = pathinfo($formValues['document_file_path'], PATHINFO_FILENAME) . '.' . $type;
276 CRM_Utils_PDF_Document::printDocuments($html, $fileName, $type, $zip);
277 }
278 else {
279 CRM_Utils_PDF_Document::html2doc($html, $fileName, $formValues);
280 }
281
282 if ($tee) {
283 $tee->stop();
284 $content = file_get_contents($tee->getFileName(), NULL, NULL, NULL, 5);
285 if (empty($content)) {
286 throw new \CRM_Core_Exception("Failed to capture document content (type=$type)!");
287 }
288 foreach ($activityIds as $activityId) {
289 civicrm_api3('Attachment', 'create', [
290 'entity_table' => 'civicrm_activity',
291 'entity_id' => $activityId,
292 'name' => $fileName,
293 'mime_type' => $mimeType,
294 'options' => [
295 'move-file' => $tee->getFileName(),
296 ],
297 ]);
298 }
299 }
300
301 $form->postProcessHook();
302
303 CRM_Utils_System::civiExit();
304 }
305
306 /**
307 * Convert from a vague-type/file-extension to mime-type.
308 *
309 * @param string $type
310 * @return string
311 * @throws \CRM_Core_Exception
312 */
313 protected function getMimeType($type) {
314 $mimeTypes = [
315 'pdf' => 'application/pdf',
316 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
317 'odt' => 'application/vnd.oasis.opendocument.text',
318 'html' => 'text/html',
319 ];
320 if (isset($mimeTypes[$type])) {
321 return $mimeTypes[$type];
322 }
323 else {
324 throw new \CRM_Core_Exception("Cannot determine mime type");
325 }
326 }
327
328 /**
329 * @param CRM_Core_Form $form
330 * @param string $html_message
331 * @param array $contactIds
332 * @param string $subject
333 * @param int $campaign_id
334 * @param array $perContactHtml
335 *
336 * @return array
337 * List of activity IDs.
338 * There may be 1 or more, depending on the system-settings
339 * and use-case.
340 *
341 * @throws CRM_Core_Exception
342 */
343 public function createActivities($form, $html_message, $contactIds, $subject, $campaign_id, $perContactHtml = []) {
344
345 $activityParams = [
346 'subject' => $subject,
347 'campaign_id' => $campaign_id,
348 'source_contact_id' => CRM_Core_Session::getLoggedInContactID(),
349 'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Print PDF Letter'),
350 'activity_date_time' => date('YmdHis'),
351 'details' => $html_message,
352 ];
353 if (!empty($form->_activityId)) {
354 $activityParams += ['id' => $form->_activityId];
355 }
356
357 $activityIds = [];
358 switch (Civi::settings()->get('recordGeneratedLetters')) {
359 case 'none':
360 return [];
361
362 case 'multiple':
363 // One activity per contact.
364 foreach ($contactIds as $i => $contactId) {
365 $fullParams = ['target_contact_id' => $contactId] + $activityParams;
366 if (!empty($form->_caseId)) {
367 $fullParams['case_id'] = $form->_caseId;
368 }
369 elseif (!empty($form->_caseIds[$i])) {
370 $fullParams['case_id'] = $form->_caseIds[$i];
371 }
372
373 if (isset($perContactHtml[$contactId])) {
374 $fullParams['details'] = implode('<hr>', $perContactHtml[$contactId]);
375 }
376 $activity = civicrm_api3('Activity', 'create', $fullParams);
377 $activityIds[$contactId] = $activity['id'];
378 }
379
380 break;
381
382 case 'combined':
383 case 'combined-attached':
384 // One activity with all contacts.
385 $fullParams = ['target_contact_id' => $contactIds] + $activityParams;
386 if (!empty($form->_caseId)) {
387 $fullParams['case_id'] = $form->_caseId;
388 }
389 elseif (!empty($form->_caseIds)) {
390 $fullParams['case_id'] = $form->_caseIds;
391 }
392 $activity = civicrm_api3('Activity', 'create', $fullParams);
393 $activityIds[] = $activity['id'];
394 break;
395
396 default:
397 throw new CRM_Core_Exception("Unrecognized option in recordGeneratedLetters: " . Civi::settings()->get('recordGeneratedLetters'));
398 }
399
400 return $activityIds;
401 }
402
0ceb63d9
EM
403 /**
404 * Handle the template processing part of the form
405 *
406 * @param array $formValues
407 *
408 * @return string $html_message
409 *
410 * @throws \CRM_Core_Exception
411 * @throws \CiviCRM_API3_Exception
412 * @throws \Civi\API\Exception\UnauthorizedException
413 */
414 public function processTemplate(&$formValues) {
415 $html_message = $formValues['html_message'] ?? NULL;
416
417 // process message template
418 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
419 $messageTemplate = [
420 'msg_text' => NULL,
421 'msg_html' => $formValues['html_message'],
422 'msg_subject' => NULL,
423 'is_active' => TRUE,
424 ];
425
426 $messageTemplate['pdf_format_id'] = 'null';
427 if (!empty($formValues['bind_format']) && $formValues['format_id']) {
428 $messageTemplate['pdf_format_id'] = $formValues['format_id'];
429 }
430 if (!empty($formValues['saveTemplate'])) {
431 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
432 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
433 }
434
435 if ($formValues['template'] && !empty($formValues['updateTemplate'])) {
436 $messageTemplate['id'] = $formValues['template'];
437
438 unset($messageTemplate['msg_title']);
439 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
440 }
441 }
442 elseif (CRM_Utils_Array::value('template', $formValues) > 0) {
443 if (!empty($formValues['bind_format']) && $formValues['format_id']) {
444 $query = "UPDATE civicrm_msg_template SET pdf_format_id = {$formValues['format_id']} WHERE id = {$formValues['template']}";
445 }
446 else {
447 $query = "UPDATE civicrm_msg_template SET pdf_format_id = NULL WHERE id = {$formValues['template']}";
448 }
449 CRM_Core_DAO::executeQuery($query);
450
451 $documentInfo = CRM_Core_BAO_File::getEntityFile('civicrm_msg_template', $formValues['template']);
452 foreach ((array) $documentInfo as $info) {
453 [$html_message, $formValues['document_type']] = CRM_Utils_PDF_Document::docReader($info['fullPath'], $info['mime_type']);
454 $formValues['document_file_path'] = $info['fullPath'];
455 }
456 }
457 // extract the content of uploaded document file
458 elseif (!empty($formValues['document_file'])) {
459 [$html_message, $formValues['document_type']] = CRM_Utils_PDF_Document::docReader($formValues['document_file']['name'], $formValues['document_file']['type']);
460 $formValues['document_file_path'] = $formValues['document_file']['name'];
461 }
462
463 if (!empty($formValues['update_format'])) {
464 $bao = new CRM_Core_BAO_PdfFormat();
465 $bao->savePdfFormat($formValues, $formValues['format_id']);
466 }
467
468 return $html_message;
469 }
470
471 /**
472 * Part of the post process which prepare and extract information from the template.
473 *
474 *
475 * @param array $formValues
476 *
477 * @return array
478 * [$categories, $html_message, $messageToken, $returnProperties]
479 */
480 public function processMessageTemplate($formValues) {
481 $html_message = $this->processTemplate($formValues);
482
483 //time being hack to strip '&nbsp;'
484 //from particular letter line, CRM-6798
485 $this->formatMessage($html_message);
486
487 $messageToken = CRM_Utils_Token::getTokens($html_message);
488
489 $returnProperties = [];
490 if (isset($messageToken['contact'])) {
491 foreach ($messageToken['contact'] as $key => $value) {
492 $returnProperties[$value] = 1;
493 }
494 }
495
496 return [$formValues, $html_message, $messageToken, $returnProperties];
497 }
498
499 /**
500 * @param $message
501 */
502 public function formatMessage(&$message) {
503 $newLineOperators = [
504 'p' => [
505 'oper' => '<p>',
506 'pattern' => '/<(\s+)?p(\s+)?>/m',
507 ],
508 'br' => [
509 'oper' => '<br />',
510 'pattern' => '/<(\s+)?br(\s+)?\/>/m',
511 ],
512 ];
513
514 $htmlMsg = preg_split($newLineOperators['p']['pattern'], $message);
515 foreach ($htmlMsg as $k => & $m) {
516 $messages = preg_split($newLineOperators['br']['pattern'], $m);
517 foreach ($messages as $key => & $msg) {
518 $msg = trim($msg);
519 $matches = [];
520 if (preg_match('/^(&nbsp;)+/', $msg, $matches)) {
521 $spaceLen = strlen($matches[0]) / 6;
522 $trimMsg = ltrim($msg, '&nbsp; ');
523 $charLen = strlen($trimMsg);
524 $totalLen = $charLen + $spaceLen;
525 if ($totalLen > 100) {
526 $spacesCount = 10;
527 if ($spaceLen > 50) {
528 $spacesCount = 20;
529 }
530 if ($charLen > 100) {
531 $spacesCount = 1;
532 }
533 $msg = str_repeat('&nbsp;', $spacesCount) . $trimMsg;
534 }
535 }
536 }
537 $m = implode($newLineOperators['br']['oper'], $messages);
538 }
539 $message = implode($newLineOperators['p']['oper'], $htmlMsg);
540 }
541
f336046d
EM
542 /**
543 * Get the buttons to display.
544 *
545 * @return array
546 */
547 protected function getButtons(): array {
548 $buttons = [];
549 if (!$this->isFormInViewMode()) {
550 $buttons[] = [
551 'type' => 'upload',
552 'name' => $this->getMainSubmitButtonName(),
553 'isDefault' => TRUE,
554 'icon' => 'fa-download',
555 ];
556 $buttons[] = [
557 'type' => 'submit',
558 'name' => ts('Preview'),
559 'subName' => 'preview',
560 'icon' => 'fa-search',
561 'isDefault' => FALSE,
562 ];
563 }
564 $buttons[] = [
565 'type' => 'cancel',
566 'name' => $this->isFormInViewMode() ? ts('Done') : ts('Cancel'),
567 ];
568 return $buttons;
569 }
570
571 /**
572 * Get the name for the main submit button.
573 *
574 * @return string
575 */
576 protected function getMainSubmitButtonName(): string {
577 return ts('Download Document');
578 }
579
fc34a273 580}