dev/core#2864 fix for rc breakage in pdf-letter-to-word
[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,
2537798f 87 ['onChange' => 'selectPaper( this.value ); showUpdateFormatChkBox();']
053c1a4b
EM
88 );
89 $form->add(
90 'select',
91 'orientation',
92 ts('Orientation'),
93 CRM_Core_BAO_PdfFormat::getPageOrientations(),
94 FALSE,
2537798f 95 ['onChange' => 'updatePaperDimensions(); showUpdateFormatChkBox();']
053c1a4b
EM
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 {
2537798f
EM
227 $formValues = $this->controller->exportValues($this->getName());
228 [$formValues, $html_message] = $this->processMessageTemplate($formValues);
fb4f4e89
EM
229 $html = $activityIds = [];
230
231 // CRM-16725 Skip creation of activities if user is previewing their PDF letter(s)
232 if ($this->isLiveMode()) {
ff811bf4 233 $activityIds = $this->createActivities($html_message, $this->_contactIds, $formValues['subject'], CRM_Utils_Array::value('campaign_id', $formValues));
fb4f4e89
EM
234 }
235
236 if (!empty($formValues['document_file_path'])) {
237 [$html_message, $zip] = CRM_Utils_PDF_Document::unzipDoc($formValues['document_file_path'], $formValues['document_type']);
238 }
239
c32592e1 240 foreach ($this->getRows() as $row) {
fb4f4e89 241 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
f34aa732 242 'contactId' => $row['contact_id'],
fb4f4e89 243 'messageTemplate' => ['msg_html' => $html_message],
4f036b71 244 'tokenContext' => array_merge(['schema' => $this->getTokenSchema()], ($row['schema'] ?? [])),
fb4f4e89
EM
245 'disableSmarty' => (!defined('CIVICRM_MAIL_SMARTY') || !CIVICRM_MAIL_SMARTY),
246 ])['html'];
247
248 $html[] = $tokenHtml;
249 }
250
251 $tee = NULL;
252 if ($this->isLiveMode() && Civi::settings()->get('recordGeneratedLetters') === 'combined-attached') {
253 if (count($activityIds) !== 1) {
2537798f 254 throw new CRM_Core_Exception('When recordGeneratedLetters=combined-attached, there should only be one activity.');
fb4f4e89
EM
255 }
256 $tee = CRM_Utils_ConsoleTee::create()->start();
257 }
258
259 $type = $formValues['document_type'];
260 $mimeType = $this->getMimeType($type);
261 // ^^ Useful side-effect: consistently throws error for unrecognized types.
262
ff811bf4 263 $fileName = $this->getFileName();
fb4f4e89
EM
264
265 if ($type === 'pdf') {
266 CRM_Utils_PDF_Utils::html2pdf($html, $fileName, FALSE, $formValues);
267 }
268 elseif (!empty($formValues['document_file_path'])) {
269 $fileName = pathinfo($formValues['document_file_path'], PATHINFO_FILENAME) . '.' . $type;
270 CRM_Utils_PDF_Document::printDocuments($html, $fileName, $type, $zip);
271 }
272 else {
d5eb8911 273 CRM_Utils_PDF_Document::html2doc($html, $fileName . '.' . $this->getSubmittedValue('document_type'), $formValues);
fb4f4e89
EM
274 }
275
276 if ($tee) {
277 $tee->stop();
278 $content = file_get_contents($tee->getFileName(), NULL, NULL, NULL, 5);
279 if (empty($content)) {
280 throw new \CRM_Core_Exception("Failed to capture document content (type=$type)!");
281 }
282 foreach ($activityIds as $activityId) {
283 civicrm_api3('Attachment', 'create', [
284 'entity_table' => 'civicrm_activity',
285 'entity_id' => $activityId,
286 'name' => $fileName,
287 'mime_type' => $mimeType,
288 'options' => [
289 'move-file' => $tee->getFileName(),
290 ],
291 ]);
292 }
293 }
294
ff811bf4 295 $this->postProcessHook();
fb4f4e89
EM
296
297 CRM_Utils_System::civiExit();
298 }
299
300 /**
301 * Convert from a vague-type/file-extension to mime-type.
302 *
303 * @param string $type
304 * @return string
305 * @throws \CRM_Core_Exception
306 */
307 protected function getMimeType($type) {
308 $mimeTypes = [
309 'pdf' => 'application/pdf',
310 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
311 'odt' => 'application/vnd.oasis.opendocument.text',
312 'html' => 'text/html',
313 ];
314 if (isset($mimeTypes[$type])) {
315 return $mimeTypes[$type];
316 }
317 else {
318 throw new \CRM_Core_Exception("Cannot determine mime type");
319 }
320 }
321
322 /**
fb4f4e89
EM
323 * @param string $html_message
324 * @param array $contactIds
325 * @param string $subject
326 * @param int $campaign_id
327 * @param array $perContactHtml
328 *
329 * @return array
330 * List of activity IDs.
331 * There may be 1 or more, depending on the system-settings
332 * and use-case.
333 *
ff811bf4
EM
334 * @throws \CRM_Core_Exception
335 * @throws \CiviCRM_API3_Exception
fb4f4e89 336 */
ff811bf4 337 protected function createActivities($html_message, $contactIds, $subject, $campaign_id, $perContactHtml = []): array {
fb4f4e89
EM
338 $activityParams = [
339 'subject' => $subject,
340 'campaign_id' => $campaign_id,
341 'source_contact_id' => CRM_Core_Session::getLoggedInContactID(),
342 'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Print PDF Letter'),
343 'activity_date_time' => date('YmdHis'),
344 'details' => $html_message,
345 ];
ff811bf4
EM
346 if (!empty($this->_activityId)) {
347 $activityParams += ['id' => $this->_activityId];
fb4f4e89
EM
348 }
349
350 $activityIds = [];
351 switch (Civi::settings()->get('recordGeneratedLetters')) {
352 case 'none':
353 return [];
354
355 case 'multiple':
356 // One activity per contact.
357 foreach ($contactIds as $i => $contactId) {
358 $fullParams = ['target_contact_id' => $contactId] + $activityParams;
ff811bf4
EM
359 if (!empty($this->_caseId)) {
360 $fullParams['case_id'] = $this->_caseId;
fb4f4e89 361 }
ff811bf4
EM
362 elseif (!empty($this->_caseIds[$i])) {
363 $fullParams['case_id'] = $this->_caseIds[$i];
fb4f4e89
EM
364 }
365
366 if (isset($perContactHtml[$contactId])) {
367 $fullParams['details'] = implode('<hr>', $perContactHtml[$contactId]);
368 }
369 $activity = civicrm_api3('Activity', 'create', $fullParams);
370 $activityIds[$contactId] = $activity['id'];
371 }
372
373 break;
374
375 case 'combined':
376 case 'combined-attached':
377 // One activity with all contacts.
378 $fullParams = ['target_contact_id' => $contactIds] + $activityParams;
ff811bf4
EM
379 if (!empty($this->_caseId)) {
380 $fullParams['case_id'] = $this->_caseId;
fb4f4e89 381 }
ff811bf4
EM
382 elseif (!empty($this->_caseIds)) {
383 $fullParams['case_id'] = $this->_caseIds;
fb4f4e89
EM
384 }
385 $activity = civicrm_api3('Activity', 'create', $fullParams);
386 $activityIds[] = $activity['id'];
387 break;
388
389 default:
ff811bf4 390 throw new CRM_Core_Exception('Unrecognized option in recordGeneratedLetters: ' . Civi::settings()->get('recordGeneratedLetters'));
fb4f4e89
EM
391 }
392
393 return $activityIds;
394 }
395
0ceb63d9
EM
396 /**
397 * Handle the template processing part of the form
398 *
399 * @param array $formValues
400 *
401 * @return string $html_message
402 *
403 * @throws \CRM_Core_Exception
404 * @throws \CiviCRM_API3_Exception
405 * @throws \Civi\API\Exception\UnauthorizedException
406 */
407 public function processTemplate(&$formValues) {
408 $html_message = $formValues['html_message'] ?? NULL;
409
410 // process message template
20fd5c17 411 if (!empty($this->getSubmittedValue('saveTemplate')) || !empty($formValues['updateTemplate'])) {
0ceb63d9
EM
412 $messageTemplate = [
413 'msg_text' => NULL,
414 'msg_html' => $formValues['html_message'],
415 'msg_subject' => NULL,
416 'is_active' => TRUE,
417 ];
418
419 $messageTemplate['pdf_format_id'] = 'null';
420 if (!empty($formValues['bind_format']) && $formValues['format_id']) {
421 $messageTemplate['pdf_format_id'] = $formValues['format_id'];
422 }
20fd5c17
EM
423 if ($this->getSubmittedValue('saveTemplate')) {
424 $messageTemplate['msg_title'] = $this->getSubmittedValue('saveTemplateName');
0ceb63d9
EM
425 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
426 }
427
428 if ($formValues['template'] && !empty($formValues['updateTemplate'])) {
429 $messageTemplate['id'] = $formValues['template'];
430
431 unset($messageTemplate['msg_title']);
432 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
433 }
434 }
435 elseif (CRM_Utils_Array::value('template', $formValues) > 0) {
436 if (!empty($formValues['bind_format']) && $formValues['format_id']) {
437 $query = "UPDATE civicrm_msg_template SET pdf_format_id = {$formValues['format_id']} WHERE id = {$formValues['template']}";
438 }
439 else {
440 $query = "UPDATE civicrm_msg_template SET pdf_format_id = NULL WHERE id = {$formValues['template']}";
441 }
442 CRM_Core_DAO::executeQuery($query);
443
444 $documentInfo = CRM_Core_BAO_File::getEntityFile('civicrm_msg_template', $formValues['template']);
9b38c821
EM
445 if ($documentInfo) {
446 $info = reset($documentInfo);
0ceb63d9
EM
447 [$html_message, $formValues['document_type']] = CRM_Utils_PDF_Document::docReader($info['fullPath'], $info['mime_type']);
448 $formValues['document_file_path'] = $info['fullPath'];
449 }
450 }
451 // extract the content of uploaded document file
452 elseif (!empty($formValues['document_file'])) {
453 [$html_message, $formValues['document_type']] = CRM_Utils_PDF_Document::docReader($formValues['document_file']['name'], $formValues['document_file']['type']);
454 $formValues['document_file_path'] = $formValues['document_file']['name'];
455 }
456
457 if (!empty($formValues['update_format'])) {
458 $bao = new CRM_Core_BAO_PdfFormat();
459 $bao->savePdfFormat($formValues, $formValues['format_id']);
460 }
461
462 return $html_message;
463 }
464
465 /**
466 * Part of the post process which prepare and extract information from the template.
467 *
468 *
469 * @param array $formValues
470 *
471 * @return array
472 * [$categories, $html_message, $messageToken, $returnProperties]
473 */
474 public function processMessageTemplate($formValues) {
475 $html_message = $this->processTemplate($formValues);
476
477 //time being hack to strip '&nbsp;'
478 //from particular letter line, CRM-6798
479 $this->formatMessage($html_message);
480
481 $messageToken = CRM_Utils_Token::getTokens($html_message);
482
483 $returnProperties = [];
484 if (isset($messageToken['contact'])) {
485 foreach ($messageToken['contact'] as $key => $value) {
486 $returnProperties[$value] = 1;
487 }
488 }
489
490 return [$formValues, $html_message, $messageToken, $returnProperties];
491 }
492
493 /**
494 * @param $message
495 */
496 public function formatMessage(&$message) {
497 $newLineOperators = [
498 'p' => [
499 'oper' => '<p>',
500 'pattern' => '/<(\s+)?p(\s+)?>/m',
501 ],
502 'br' => [
503 'oper' => '<br />',
504 'pattern' => '/<(\s+)?br(\s+)?\/>/m',
505 ],
506 ];
507
508 $htmlMsg = preg_split($newLineOperators['p']['pattern'], $message);
509 foreach ($htmlMsg as $k => & $m) {
510 $messages = preg_split($newLineOperators['br']['pattern'], $m);
511 foreach ($messages as $key => & $msg) {
512 $msg = trim($msg);
513 $matches = [];
514 if (preg_match('/^(&nbsp;)+/', $msg, $matches)) {
515 $spaceLen = strlen($matches[0]) / 6;
516 $trimMsg = ltrim($msg, '&nbsp; ');
517 $charLen = strlen($trimMsg);
518 $totalLen = $charLen + $spaceLen;
519 if ($totalLen > 100) {
520 $spacesCount = 10;
521 if ($spaceLen > 50) {
522 $spacesCount = 20;
523 }
524 if ($charLen > 100) {
525 $spacesCount = 1;
526 }
527 $msg = str_repeat('&nbsp;', $spacesCount) . $trimMsg;
528 }
529 }
530 }
531 $m = implode($newLineOperators['br']['oper'], $messages);
532 }
533 $message = implode($newLineOperators['p']['oper'], $htmlMsg);
534 }
535
f336046d
EM
536 /**
537 * Get the buttons to display.
538 *
539 * @return array
540 */
541 protected function getButtons(): array {
542 $buttons = [];
543 if (!$this->isFormInViewMode()) {
544 $buttons[] = [
545 'type' => 'upload',
546 'name' => $this->getMainSubmitButtonName(),
547 'isDefault' => TRUE,
548 'icon' => 'fa-download',
549 ];
550 $buttons[] = [
551 'type' => 'submit',
552 'name' => ts('Preview'),
553 'subName' => 'preview',
554 'icon' => 'fa-search',
555 'isDefault' => FALSE,
556 ];
557 }
558 $buttons[] = [
559 'type' => 'cancel',
560 'name' => $this->isFormInViewMode() ? ts('Done') : ts('Cancel'),
561 ];
562 return $buttons;
563 }
564
565 /**
566 * Get the name for the main submit button.
567 *
568 * @return string
569 */
570 protected function getMainSubmitButtonName(): string {
571 return ts('Download Document');
572 }
573
fc34a273 574}