diff --git a/Civi/ActionProvider/Action/Communication/CreatePdf.php b/Civi/ActionProvider/Action/Communication/CreatePdf.php index 0986a6d963677c3a312cb4d102b7a6cc9c713b2d..bc517646cee16636bd381a06c6c4cd4b8fd40d66 100644 --- a/Civi/ActionProvider/Action/Communication/CreatePdf.php +++ b/Civi/ActionProvider/Action/Communication/CreatePdf.php @@ -7,11 +7,11 @@ namespace Civi\ActionProvider\Action\Communication; use Civi\ActionProvider\Action\AbstractAction; +use Civi\ActionProvider\Parameter\OptionGroupByNameSpecification; use \Civi\ActionProvider\Parameter\ParameterBagInterface; use Civi\ActionProvider\Parameter\SpecificationBag; use Civi\ActionProvider\Parameter\Specification; - -use Civi\ActionProvider\Utils\FileWriter; +use Civi\ActionProvider\Utils\Files; use CRM_ActionProvider_ExtensionUtil as E; class CreatePdf extends AbstractAction { @@ -21,43 +21,47 @@ class CreatePdf extends AbstractAction { */ protected $zip; + protected $messages = array(); + + protected $pdfLetterActivityType; + public function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) { - $domain = \CRM_Core_BAO_Domain::getDomain(); $message = $parameters->getParameter('message'); $contactId = $parameters->getParameter('contact_id'); $filename = $this->configuration->getParameter('filename'); $fileNameWithoutContactId = $filename . '.pdf'; $filenameWithContactId = $filename . '_' . $contactId . '.pdf'; - $subdir = $this->createSubDir($this->currentBatch); - - $contact = civicrm_api3('Contact', 'getsingle', array('id' => $contactId)); - - $tokens = \CRM_Utils_Token::getTokens($message); - - \CRM_Utils_Hook::tokenValues($contact, $contactId, NULL, $tokens); - // call token hook - $hookTokens = array(); - \CRM_Utils_Hook::tokens($hookTokens); - $categories = array_keys($hookTokens); - - $message = \CRM_Utils_Token::replaceDomainTokens($message, $domain, TRUE, $tokens, TRUE); - $message = \CRM_Utils_Token::replaceHookTokens($message, $contact, $categories, TRUE); - \CRM_Utils_Token::replaceGreetingTokens($message, $contact, $contactId); - $message = \CRM_Utils_Token::replaceContactTokens($message, $contact, FALSE, $tokens, FALSE, TRUE); - $message = \CRM_Utils_Token::replaceComponentTokens($message, $contact, $tokens, TRUE); - if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY) { - $smarty = \CRM_Core_Smarty::singleton(); - $message = $smarty->fetch("string:{$message}"); + $processedMessage = $this->getProcessedMessage($contactId, $message); + if ($processedMessage === false) { + return; } + $this->messages[] = $processedMessage; + $pdfContents = \CRM_Utils_PDF_Utils::html2pdf(array($processedMessage), $fileNameWithoutContactId, TRUE); - $contents = \CRM_Utils_PDF_Utils::html2pdf($message, $filenameWithContactId, TRUE); - if ($this->zip) { - $this->zip->addFromString($filenameWithContactId, $contents); + if ($this->currentBatch && $this->zip) { + $this->zip->addFromString($filenameWithContactId, $pdfContents); } + $file = $this->createActivity($contactId, $message, $pdfContents, $fileNameWithoutContactId); + + $output->setParameter('filename', $file['name']); + $output->setParameter('url', $file['url']); + $output->setParameter('path', $file['path']); + } + + /** + * @param $contactId + * @param $message + * @param $pdfContents + * @param $filename + * @return array + * Returns the file array + */ + protected function createActivity($contactId, $message, $pdfContents, $filename) { + $activityTypeId = $this->getPdfLetterActivityTypeId(); $activityParams = array( - 'activity_type_id' => \CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Print PDF Letter'), + 'activity_type_id' => $activityTypeId, 'activity_date_time' => date('YmdHis'), 'details' => $message, 'target_contact_id' => $contactId, @@ -66,16 +70,77 @@ class CreatePdf extends AbstractAction { $attachment = civicrm_api3('Attachment', 'create', array( 'entity_table' => 'civicrm_activity', 'entity_id' => $result['id'], - 'name' => $fileNameWithoutContactId, + 'name' => $filename, 'mime_type' => 'application/pdf', - 'content' => $contents, + 'content' => $pdfContents, )); - $file = reset($attachment['values']); + return reset($attachment['values']); + } - $output->setParameter('filename', $file['name']); - $output->setParameter('url', $file['url']); - $output->setParameter('path', $file['path']); + /** + * @return array + * @throws \CiviCRM_API3_Exception + */ + protected function getPdfLetterActivityTypeId() { + if (!$this->pdfLetterActivityType) { + $activityTypeName = $this->configuration->getParameter('activity_type_id'); + $this->pdfLetterActivityType = civicrm_api3('OptionValue', 'getvalue', [ + 'option_group_id' => 'activity_type', + 'name' => $activityTypeName, + 'return' => 'value' + ]); + } + return $this->pdfLetterActivityType; + } + + /** + * Returns a processed message. Meaning that all tokens are replaced with their value. + * This message could then be used to generate the PDF. + * + * @param $contactId + * @param $message + * @return string + */ + protected function getProcessedMessage($contactId, $message) { + $tokenCategories = self::getTokenCategories(); + //time being hack to strip ' ' + //from particular letter line, CRM-6798 + \CRM_Contact_Form_Task_PDFLetterCommon::formatMessage($message); + $messageToken = \CRM_Utils_Token::getTokens($message); + + $returnProperties = array(); + if (isset($messageToken['contact'])) { + foreach ($messageToken['contact'] as $key => $value) { + $returnProperties[$value] = 1; + } + } + + $params = array('contact_id' => $contactId); + list($contact) = \CRM_Utils_Token::getTokenDetails($params, + $returnProperties, + false, + false, + NULL, + $messageToken, + null + ); + + if (civicrm_error($contact)) { + return false; + } + + $tokenHtml = \CRM_Utils_Token::replaceContactTokens($message, $contact[$contactId], TRUE, $messageToken); + $tokenHtml = \CRM_Utils_Token::replaceHookTokens($tokenHtml, $contact[$contactId], $tokenCategories, TRUE); + + if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY) { + $smarty = \CRM_Core_Smarty::singleton(); + // also add the contact tokens to the template + $smarty->assign_by_ref('contact', $contact); + $tokenHtml = $smarty->fetch("string:$tokenHtml"); + } + + return $tokenHtml; } /** @@ -84,15 +149,14 @@ class CreatePdf extends AbstractAction { * @param $batchName */ public function initializeBatch($batchName) { - // Child classes could override this function - // E.g. create a directory - $this->createSubDir($batchName); - - $subdir = $this->createSubDir(); - $outputName = \CRM_Core_Config::singleton()->templateCompileDir . $subdir.'/'.$batchName.'.zip'; - $this->zip = new \ZipArchive(); - if ($this->zip->open($outputName, \ZipArchive::CREATE) !== TRUE) { - $this->zip = null; + $outputMode = $this->configuration->getParameter('batch_output_mode'); + if ($outputMode == 'zip') { + $subdir = Files::createRestrictedDirectory('createpdf'); + $outputName = \CRM_Core_Config::singleton()->templateCompileDir . $subdir . '/' . $batchName . '.zip'; + $this->zip = new \ZipArchive(); + if ($this->zip->open($outputName, \ZipArchive::CREATE) !== TRUE) { + $this->zip = NULL; + } } $this->currentBatch = $batchName; @@ -108,32 +172,44 @@ class CreatePdf extends AbstractAction { public function finishBatch($batchName, $isLastBatch=false) { // Child classes could override this function // E.g. merge files in a directorys - $subdir = $this->createSubDir(); - $downloadName = $this->configuration->getParameter('filename').'.zip'; if ($this->zip) { $this->zip->close(); if ($isLastBatch) { - $downloadUrl = \CRM_Utils_System::url('civicrm/actionprovider/downloadfile', [ - 'filename' => $batchName . '.zip', - 'subdir' => $subdir, - 'downloadname' => $downloadName - ]); - \CRM_Core_Session::setStatus(E::ts('<a href="%1">Download document(s)<a/>', [1 => $downloadUrl]), E::ts('Created PDF'), 'success'); + $subdir = Files::createRestrictedDirectory('createpdf'); + $downloadName = $this->configuration->getParameter('filename').'.zip'; + $this->createDownloadStatusMessage($batchName.'.zip', $subdir, $downloadName); + } + } else { + $subdir = Files::createRestrictedDirectory('createpdf'); + $basePath = \CRM_Core_Config::singleton()->templateCompileDir . $subdir; + $htmlFile = $basePath .'/'.$batchName.'.html'; + if (count($this->messages)) { + $this->addPagesToHtmlFile($this->messages, $htmlFile); + } + if ($isLastBatch) { + $pdfFile = $basePath .'/'.$batchName.'.pdf'; + $this->convertHtmlToPdf($htmlFile, $pdfFile); + $downloadName = $this->configuration->getParameter('filename').'.pdf'; + $this->createDownloadStatusMessage($batchName.'.pdf', $subdir, $downloadName); } } } - protected function createSubDir() { - $subDir = 'action_provider'; - $basePath = \CRM_Core_Config::singleton()->templateCompileDir . $subDir; - \CRM_Utils_File::createDir($basePath); - \CRM_Utils_File::restrictAccess($basePath.'/'); - $subDir .= '/createpdf'; - $basePath = \CRM_Core_Config::singleton()->templateCompileDir . $subDir; - \CRM_Utils_File::createDir($basePath); - \CRM_Utils_File::restrictAccess($basePath.'/'); - return $subDir; + /** + * Creates a status message with a download link. + * + * @param $filename + * @param $subdir + * @param $downloadName + */ + protected function createDownloadStatusMessage($filename, $subdir, $downloadName) { + $downloadUrl = \CRM_Utils_System::url('civicrm/actionprovider/downloadfile', [ + 'filename' => $filename, + 'subdir' => $subdir, + 'downloadname' => $downloadName + ]); + \CRM_Core_Session::setStatus(E::ts('<a href="%1">Download document(s)<a/>', [1 => $downloadUrl]), E::ts('Created PDF'), 'success'); } /** @@ -149,8 +225,19 @@ class CreatePdf extends AbstractAction { } public function getConfigurationSpecification() { + $filename = new Specification('filename', 'String', E::ts('Filename'), true, E::ts('document')); + $filename->setDescription(E::ts('Without the extension .pdf or .zip')); + $batch_output_mode = new Specification('batch_output_mode', 'String', E::ts("Batch output mode"), true, 'pdf', null, array( + 'zip' => E::ts('All files in one zip'), + 'pdf' => E::ts('Merge all files into one PDF'), + )); + $batch_output_mode->setDescription(E::ts('When this action is executed in batch mode, meaning that it generates more than one pdf, in which way do you want to retrieve the generated PDFs')); + $activity = new OptionGroupByNameSpecification('activity_type_id', 'activity_type', E::ts('PDF Letter Activity'), true, 'Print PDF Letter'); + return new SpecificationBag(array( - new Specification('filename', 'String', E::ts('Filename'), true, E::ts('document')), + $filename, + $batch_output_mode, + $activity )); } @@ -162,5 +249,179 @@ class CreatePdf extends AbstractAction { )); } + /** + * Returns a help text for this action. + * + * The help text is shown to the administrator who is configuring the action. + * Override this function in a child class if your action has a help text. + * + * @return string|false + */ + public function getHelpText() { + return E::ts(" + This action generates PDF files for the contacts. <br /> + When this action is used in a batch you can define how you want to download the + generated PDFs: either in one zip file or in one pdf. <br /> + <br /> + The input for this action is a contact ID and the message. You can use the <em>Find Message Template by name</em> action + to retrieve the message. + "); + } + + /** + * Get the categories required for rendering tokens. + * + * @return array + */ + protected static function getTokenCategories() { + if (!isset(\Civi::$statics[__CLASS__]['token_categories'])) { + $tokens = array(); + \CRM_Utils_Hook::tokens($tokens); + \Civi::$statics[__CLASS__]['token_categories'] = array_keys($tokens); + } + return \Civi::$statics[__CLASS__]['token_categories']; + } + + /** + * Initialize an HTML file. The HTML file is later on converted to a PDF. + * + * Function taken from CRM_Utils_PDF_Utils::html2pdf + * + * @return string + */ + protected function initializeHtmlFile() { + $format = \CRM_Core_BAO_PdfFormat::getDefaultValues(); + $metric = \CRM_Core_BAO_PdfFormat::getValue('metric', $format); + $t = \CRM_Core_BAO_PdfFormat::getValue('margin_top', $format); + $r = \CRM_Core_BAO_PdfFormat::getValue('margin_right', $format); + $b = \CRM_Core_BAO_PdfFormat::getValue('margin_bottom', $format); + $l = \CRM_Core_BAO_PdfFormat::getValue('margin_left', $format); + + // Add a special region for the HTML header of PDF files: + $pdfHeaderRegion = \CRM_Core_Region::instance('export-document-header', FALSE); + $htmlHeader = ($pdfHeaderRegion) ? $pdfHeaderRegion->render('', FALSE) : ''; + + $html = " +<html> + <head> + <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/> + <style>@page { margin: {$t}{$metric} {$r}{$metric} {$b}{$metric} {$l}{$metric}; }</style> + <style type=\"text/css\">@import url(" . \CRM_Core_Config::singleton()->userFrameworkResourceURL . "css/print.css);</style> + {$htmlHeader} + </head> + <body> + <div id=\"crm-container\">\n"; + return $html; + } + + /** + * Convert an array of pages to html and append it to an already existing html file + * + * Function taken from CRM_Utils_PDF_Utils::html2pdf + * + * @param $pages + * @param $html_file + */ + protected function addPagesToHtmlFile($pages, $html_file) { + $html = ""; + if (!file_exists($html_file)) { + $html = $this->initializeHtmlFile(); + } else { + // Append a line break to the file before adding the pages. + $html = "\n<div style=\"page-break-after: always\"></div>\n"; + } + // Strip <html>, <header>, and <body> tags from each page + $htmlElementstoStrip = [ + '@<head[^>]*?>.*?</head>@siu', + '@<script[^>]*?>.*?</script>@siu', + '@<body>@siu', + '@</body>@siu', + '@<html[^>]*?>@siu', + '@</html>@siu', + '@<!DOCTYPE[^>]*?>@siu', + ]; + $htmlElementsInstead = ['', '', '', '', '', '']; + foreach ($pages as & $page) { + $page = preg_replace($htmlElementstoStrip, + $htmlElementsInstead, + $page + ); + } + // Glue the pages together + $html .= implode("\n<div style=\"page-break-after: always\"></div>\n", $pages); + file_put_contents($html_file, $html, FILE_APPEND); + } + + protected function convertHtmlToPdf($html_file, $output_file) { + $html = "</div></body></html>"; + file_put_contents($html_file, $html, FILE_APPEND); + + $format = \CRM_Core_BAO_PdfFormat::getDefaultValues(); + $paperSize = \CRM_Core_BAO_PaperSize::getByName($format['paper_size']); + $paper_width = \CRM_Utils_PDF_Utils::convertMetric($paperSize['width'], $paperSize['metric'], 'pt'); + $paper_height = \CRM_Utils_PDF_Utils::convertMetric($paperSize['height'], $paperSize['metric'], 'pt'); + // dompdf requires dimensions in points + $paper_size = array(0, 0, $paper_width, $paper_height); + $orientation = \CRM_Core_BAO_PdfFormat::getValue('orientation', $format); + $metric = \CRM_Core_BAO_PdfFormat::getValue('metric', $format); + $t = \CRM_Core_BAO_PdfFormat::getValue('margin_top', $format); + $r = \CRM_Core_BAO_PdfFormat::getValue('margin_right', $format); + $b = \CRM_Core_BAO_PdfFormat::getValue('margin_bottom', $format); + $l = \CRM_Core_BAO_PdfFormat::getValue('margin_left', $format); + + $margins = array($metric, $t, $r, $b, $l); + + if (\CRM_Core_Config::singleton()->wkhtmltopdfPath) { + $this->_html2pdf_wkhtmltopdf($paper_size, $orientation, $margins, $html_file, $output_file); + } + else { + $this->_html2pdf_dompdf($paper_size, $orientation, $html_file, $output_file); + } + unlink($html_file); + } + + /** + * @param $paper_size + * @param $orientation + * @param $margins + * @param $html_file + * @param string $fileName + */ + protected function _html2pdf_wkhtmltopdf($paper_size, $orientation, $margins, $html_file, $fileName) { + require_once 'packages/snappy/src/autoload.php'; + $config = \CRM_Core_Config::singleton(); + $snappy = new \Knp\Snappy\Pdf($config->wkhtmltopdfPath); + $snappy->setOption("page-width", $paper_size[2] . "pt"); + $snappy->setOption("page-height", $paper_size[3] . "pt"); + $snappy->setOption("orientation", $orientation); + $snappy->setOption("margin-top", $margins[1] . $margins[0]); + $snappy->setOption("margin-right", $margins[2] . $margins[0]); + $snappy->setOption("margin-bottom", $margins[3] . $margins[0]); + $snappy->setOption("margin-left", $margins[4] . $margins[0]); + $pdf = $snappy->generate($html_file, $fileName); + } + + /** + * @param $paper_size + * @param $orientation + * @param $html_file + * @param string $fileName + * + * @return string + */ + protected function _html2pdf_dompdf($paper_size, $orientation, $html_file, $fileName) { + // CRM-12165 - Remote file support required for image handling. + $options = new \Dompdf\Options(); + $options->set('isRemoteEnabled', TRUE); + $options->set('chroot', dirname($html_file)); + + $dompdf = new \Dompdf\Dompdf($options); + $dompdf->set_paper($paper_size, $orientation); + $dompdf->loadHtmlFile($html_file); + $dompdf->render(); + + file_put_contents($fileName, $dompdf->output()); + } + } \ No newline at end of file diff --git a/Civi/ActionProvider/Utils/Files.php b/Civi/ActionProvider/Utils/Files.php index f77f9141f148e225c04b24b26f92287c1f7f0ce9..c441b6f1c5c15604d1df7cc1270c3789149430fa 100644 --- a/Civi/ActionProvider/Utils/Files.php +++ b/Civi/ActionProvider/Utils/Files.php @@ -6,27 +6,27 @@ namespace Civi\ActionProvider\Utils; -class FileWriter { +class Files { /** - * Write to a file in a restricted directory. E.g. a directory which is not accesible by - * a url. + * Creates a directory under templates_c/action_provider + * And adds a .htaccess to this directory so files could not be downloaded directly from the server. * - * @param $contents - * @param $filename - * @param $subDir + * @param string $dir + * The subdirectory without / on the end. E.g. createpdf or createpdf/batch1233 * @return string - * The full file path. */ - public static function writeFile($contents, $filename, $subDir) { - $basePath = \CRM_Core_Config::singleton()->templateCompileDir . $subDir; - \CRM_Utils_File::createDir($basePath); - \CRM_Utils_File::restrictAccess($basePath.'/'); - $fullFilePath = $basePath.'/'. $filename; + public static function createRestrictedDirectory($dir) { + $subDir = 'action_provider/'.$dir; + $fullPath = \CRM_Core_Config::singleton()->templateCompileDir . $subDir; + if (is_dir($fullPath)) { + \CRM_Utils_File::restrictAccess($fullPath.'/'); + return $subDir; + } - file_put_contents($fullFilePath, $contents); - - return $fullFilePath; + \CRM_Utils_File::createDir($fullPath); + \CRM_Utils_File::restrictAccess($fullPath.'/'); + return $subDir; } } \ No newline at end of file