From 9b50cda010f1cdace3525cfa092692203181ff3c Mon Sep 17 00:00:00 2001 From: Bernd Oliver Suenderhauf <bos@suenderhauf.de> Date: Wed, 5 Jun 2019 00:33:00 +0200 Subject: [PATCH] Issue #3048947 by Pancho, wizonesolutions: Refactor HandlePdfController::populatePdf() --- fillpdf.services.yml | 2 +- src/Controller/HandlePdfController.php | 231 +++++++----------- src/FieldMapping.php | 16 +- src/FieldMapping/ImageFieldMapping.php | 24 +- src/FieldMapping/TextFieldMapping.php | 15 +- src/FillPdfBackendManager.php | 3 + src/FillPdfBackendPluginInterface.php | 21 +- src/FillPdfContextManagerInterface.php | 6 +- src/OutputHandler.php | 2 +- .../FillPdfServiceFillPdfBackend.php | 28 ++- .../FillPdfBackend/LocalFillPdfBackend.php | 20 +- src/Plugin/FillPdfBackend/LocalService.php | 51 +--- .../FillPdfBackend/PdftkFillPdfBackend.php | 12 +- src/TokenResolver.php | 184 ++++++++++++-- src/TokenResolverInterface.php | 29 ++- 15 files changed, 374 insertions(+), 270 deletions(-) diff --git a/fillpdf.services.yml b/fillpdf.services.yml index 4906028..a538517 100644 --- a/fillpdf.services.yml +++ b/fillpdf.services.yml @@ -35,7 +35,7 @@ services: fillpdf.token_resolver: class: Drupal\fillpdf\TokenResolver - arguments: ["@token", "@token.entity_mapper"] + arguments: ["@token", "@token.entity_mapper", "@module_handler"] fillpdf.entity_helper: class: Drupal\fillpdf\EntityHelper diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php index 9239958..01640ac 100644 --- a/src/Controller/HandlePdfController.php +++ b/src/Controller/HandlePdfController.php @@ -2,23 +2,24 @@ namespace Drupal\fillpdf\Controller; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\StreamWrapper\StreamWrapperInterface; -use Drupal\file\Entity\File; -use Drupal\file\FileInterface; -use Drupal\fillpdf\Component\Helper\FillPdfMappingHelper; -use Drupal\fillpdf\Entity\FillPdfForm; use Drupal\fillpdf\FillPdfBackendManager; use Drupal\fillpdf\FillPdfContextManagerInterface; use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfLinkManipulatorInterface; -use Drupal\fillpdf\Plugin\FillPdfActionPluginManager; use Drupal\fillpdf\TokenResolverInterface; +use Drupal\fillpdf\Component\Helper\FillPdfMappingHelper; +use Drupal\fillpdf\Entity\FillPdfForm; +use Drupal\fillpdf\FieldMapping\ImageFieldMapping; +use Drupal\fillpdf\FieldMapping\TextFieldMapping; +use Drupal\fillpdf\Plugin\FillPdfActionPluginManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; -use Drupal\Core\Link; /** * Class HandlePdfController. @@ -117,125 +118,61 @@ class HandlePdfController extends ControllerBase { * validation. */ public function populatePdf() { - $context = $this->linkManipulator->parseRequest($this->requestStack->getCurrentRequest()); - $config = $this->config('fillpdf.settings'); - $fillpdf_service = $config->get('backend'); - // Load the backend plugin. - /** @var \Drupal\fillpdf\FillPdfBackendPluginInterface $backend */ - $backend = $this->backendManager->createInstance($fillpdf_service, $config->get()); + $context = $this->linkManipulator->parseRequest($this->requestStack->getCurrentRequest()); + $entities = $this->contextManager->loadEntities($context); - // @todo: Emit event (or call alter hook?) before populating PDF. - // Rename fillpdf_merge_fields_alter() to fillpdf_populate_fields_alter(). $fillpdf_form = FillPdfForm::load($context['fid']); $form_replacements = FillPdfMappingHelper::parseReplacements($fillpdf_form->replacements->value); - $fields = $fillpdf_form->getFormFields(); - // Populate entities array based on what user passed in. - $entities = $this->contextManager->loadEntities($context); - - $field_mapping = [ - 'fields' => [], - 'images' => [], - ]; - - $mapped_fields = &$field_mapping['fields']; - $image_data = &$field_mapping['images']; - foreach ($fields as $pdf_key => $field) { + // Populate mappings array. + $field_mappings = []; + foreach ($fillpdf_form->getFormFields() as $pdf_key => $field) { if ($context['sample']) { - $mapped_fields[$pdf_key] = $pdf_key; + $field_mappings[$pdf_key] = new TextFieldMapping($pdf_key); } else { - // Get image fields attached to the entity and derive their token names - // based on the entity types we are working with at the moment. - $fill_pattern = count($field->value) ? $field->value->value : ''; - $is_image_token = FALSE; - foreach ($entities as $entity_type => $entities_of_that_type) { - $lifo_entities = array_reverse($entities_of_that_type); - foreach ($lifo_entities as $entity) { - if (method_exists($entity, 'getFields')) { - // Translate entity type into token type. - $token_type = $this->tokenResolver->getEntityMapper()->getTokenTypeForEntityType($entity_type); - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - foreach ($entity->getFields() as $field_name => $field_data) { - $field_definition = $field_data->getFieldDefinition(); - if ($field_definition->getType() === 'image') { - if ($fill_pattern === "[{$token_type}:{$field_name}]") { - // It's a match! - $is_image_token = TRUE; - if (count($entity->{$field_name})) { - $image_file = File::load($entity->{$field_name}->target_id); - $this->processImageTokens($image_file, $mapped_fields, $pdf_key, $image_data); - } - } - } - } - } - // Also support (non-chained) Webform submission element tokens, - // which are not fields. - if ($entity_type === 'webform_submission') { - // We can iterate over this submission's Webform's elements and - // manually match for patterns. We support signature and image - // fields. - /** @var \Drupal\webform\WebformSubmissionInterface $entity */ - $webform = $entity->getWebform(); - $webform_field_data = array_filter($webform->getElementsInitializedFlattenedAndHasValue(), function ($value) { - return (!empty($value) && $value['#type'] === 'webform_image_file'); - }); - $submission_values = $entity->getData(); - $data_keys = array_keys($webform_field_data); - foreach ($data_keys as $webform_field_name) { - if ($fill_pattern === "[webform_submission:values:{$webform_field_name}]") { - $webform_image_file = File::load($submission_values[$webform_field_name]); - if (!$webform_image_file) { - break; - } - - $is_image_token = TRUE; - $this->processImageTokens($webform_image_file, $mapped_fields, $pdf_key, $image_data); - } - } - } - } - } - - if (!$is_image_token) { - // Resolve tokens. - $replaced_string = $this->tokenResolver->replace($fill_pattern, $entities); - - // Replace <br /> occurrences with newlines. - $replaced_string = preg_replace('|<br />|', ' -', $replaced_string); - - // Apply transformations with field-level replacements taking - // precedence over form-level replacements. - $field_replacements = FillPdfMappingHelper::parseReplacements($field->replacements->value) + $form_replacements; - if (isset($field_replacements[$replaced_string])) { - $replaced_string = $field_replacements[$replaced_string]; - } - - // Apply prefix and suffix, if applicable. - if (isset($replaced_string) && $replaced_string) { - if ($field->prefix->value) { - $replaced_string = $field->prefix->value . $replaced_string; - } - if ($field->suffix->value) { - $replaced_string .= $field->suffix->value; - } - } - - $mapped_fields[$pdf_key] = $replaced_string; - } + $options = []; + // Our pdftk backend doesn't support image stamping, so at least for + // this backend we already know which type of content we can expect. + $options['content'] = $config->get('backend') == 'pdftk' ? 'text' : ''; + + // Prepare transformations with field-level replacements taking + // precedence over form-level replacements. + $options['replacements'] = FillPdfMappingHelper::parseReplacements($field->replacements->value) + $form_replacements; + + // Add prefix and suffix. + $options['prefix'] = $field->prefix->value; + $options['suffix'] = $field->suffix->value; + + // Resolve tokens. + $text = count($field->value) ? $field->value->value : ''; + $field_mappings[$pdf_key] = $this->tokenResolver->replace($text, $entities, $options); } } - $title_pattern = $fillpdf_form->title->value; // Generate the filename of downloaded PDF from title of the PDF set in // admin/structure/fillpdf/%fid. - $filename = $this->buildFilename($title_pattern, $entities); + $filename = $this->buildFilename($fillpdf_form->title->value, $entities); + + // Now load the backend plugin. + $backend = $this->backendManager->createInstance($config->get('backend'), $config->get()); + + // Turn FieldMapping objects back into legacy field mapping arrays for BC. + // Current backend plugins implement PluginInspectionInterface and are at + // API version 4.5. + // @todo Remove this BC layer before 8.x-5.x. + $is_current_api = $backend instanceof PluginInspectionInterface && $backend->getPluginDefinition()['api'] == 4.5; + if (!$is_current_api) { + $field_mappings = static::legacyFieldMapping($field_mappings); + } + + // @todo: Emit event (or call alter hook?) before populating PDF. + // Rename fillpdf_merge_fields_alter() to fillpdf_populate_fields_alter(). + /** @var \Drupal\fillpdf\FillPdfBackendPluginInterface $backend */ + $populated_pdf = $backend->populateWithFieldData($fillpdf_form, $field_mappings, $context); - $populated_pdf = $backend->populateWithFieldData($fillpdf_form, $field_mapping, $context); if (empty($populated_pdf)) { $this->messenger()->addError($this->t('Merging the FillPDF Form failed.')); return new RedirectResponse(Url::fromRoute('<front>')->toString()); @@ -254,10 +191,13 @@ class HandlePdfController extends ControllerBase { * The original filename without tokens being replaced. * @param \Drupal\Core\Entity\EntityInterface[] $entities * An array of entities to be used for replacing tokens. + * + * @return string + * The token-replaced filename. */ protected function buildFilename($original, array $entities) { // Replace tokens *before* sanitization. - $original = $this->tokenResolver->replace($original, $entities); + $original = (string) $this->tokenResolver->replace($original, $entities, ['content' => 'text']); $output_name = str_replace(' ', '_', $original); $output_name = preg_replace('/\.pdf$/i', '', $output_name); @@ -355,44 +295,43 @@ class HandlePdfController extends ControllerBase { } /** - * Processes image tokens. + * Converts FieldMapping objects into legacy field mapping arrays. + * + * @param \Drupal\fillpdf\FieldMapping[] $field_mappings + * The field mappings. + * + * @return array + * A nested associative array containing: + * - 'fields': Associative array of text mappings keyed by pdf_key + * - 'images': A nested associative array keyed by pdf_key, the values + * being themselves associative arrays with the following keys: + * - 'data': The base64 encoded image data + * - 'filenamehash': md5-hash of the filename with the original + * extension. * - * @param \Drupal\file\FileInterface $image_file - * Image file object. - * @param string[] $mapped_fields - * Array of mapped fields. - * @param string $pdf_key - * PDF key. - * @param array[] $image_data - * Array of image data, keyed by the PDF key. + * @internal + * @todo Remove in 8.x-5.x. Backends have different requirements, so should + * handle image files themselves. */ - protected function processImageTokens(FileInterface $image_file, array &$mapped_fields, $pdf_key, array &$image_data) { - $backend = $this->config('fillpdf.settings')->get('backend'); - - // @todo Refactor in 8.x-5.x. Pdftk doesn't support image stamping. - if ($backend == 'pdftk') { - return; - } - - $image_path = $image_file->getFileUri(); - $mapped_fields[$pdf_key] = "{image}{$image_path}"; - - // @todo Refactor in 8.x-5.x. Local and LocalService backends handle image - // files themselves. So this only remains in place for FillPdfService - // and possible third-party backend plugins. - if (in_array($backend, ['local_service', 'local'])) { - return; - } - - $image_path_info = pathinfo($image_path); - // Store the image data to transmit to the remote service if necessary. - $file_data = file_get_contents($image_path); - if ($file_data) { - $image_data[$pdf_key] = [ - 'data' => base64_encode($file_data), - 'filenamehash' => md5($image_path_info['filename']) . '.' . $image_path_info['extension'], - ]; + private static function legacyFieldMapping(array $field_mappings) { + $legacy_mapping = []; + foreach ($field_mappings as $pdf_key => $mapping) { + if ($mapping instanceof TextFieldMapping) { + $legacy_mapping['fields'][$pdf_key] = (string) $mapping->getData(); + } + elseif ($mapping instanceof ImageFieldMapping) { + $uri = (string) $mapping->getUri(); + if ($uri) { + $legacy_mapping['fields'][$pdf_key] = "{image}{$uri}"; + $image_path_info = pathinfo($uri); + $legacy_mapping['images'][$pdf_key] = [ + 'data' => base64_encode($mapping->getData()), + 'filenamehash' => md5($image_path_info['filename']) . '.' . $image_path_info['extension'], + ]; + } + } } + return $legacy_mapping; } } diff --git a/src/FieldMapping.php b/src/FieldMapping.php index 8b6470e..63c6ea2 100644 --- a/src/FieldMapping.php +++ b/src/FieldMapping.php @@ -13,16 +13,28 @@ namespace Drupal\fillpdf; abstract class FieldMapping { /** - * @var mixed - * * The primary value of the mapping. + * + * @var string */ protected $data; + /** + * Constructs a FieldMapping object. + * + * @param string $data + * String containing the data. + */ public function __construct($data) { $this->data = $data; } + /** + * Returns the data. + * + * @return string + * String containing the data. + */ public function getData() { return $this->data; } diff --git a/src/FieldMapping/ImageFieldMapping.php b/src/FieldMapping/ImageFieldMapping.php index 2f30013..5f2615d 100644 --- a/src/FieldMapping/ImageFieldMapping.php +++ b/src/FieldMapping/ImageFieldMapping.php @@ -21,6 +21,13 @@ class ImageFieldMapping extends FieldMapping { */ protected $extension; + /** + * The image file's URI. + * + * @var string + */ + protected $uri; + /** * Constructs an ImageFieldMapping object. * @@ -31,18 +38,21 @@ class ImageFieldMapping extends FieldMapping { * (optional) The original extension corresponding to the image data. If the * backend doesn't need to know the extension and you don't want extensions * to be checked, you can leave it blank. + * @param string $uri + * (optional) URI of the image. * * @throws \InvalidArgumentException * If the extension isn't one of 'jpg', 'png', or 'gif'. */ - public function __construct($data, $extension = NULL) { + public function __construct($data, $extension = NULL, $uri = NULL) { parent::__construct($data); if (isset($extension) && !in_array($extension, ['jpg', 'png', 'gif'])) { throw new \InvalidArgumentException('Extension must be one of: jpg, png, gif.'); } - $this->extension = $extension; + + $this->uri = $uri; } /** @@ -55,4 +65,14 @@ class ImageFieldMapping extends FieldMapping { return $this->extension; } + /** + * Gets the image file's URI. + * + * @return string + * The file's URI. + */ + public function getUri() { + return $this->uri; + } + } diff --git a/src/FieldMapping/TextFieldMapping.php b/src/FieldMapping/TextFieldMapping.php index 00fcc5d..6fd4585 100644 --- a/src/FieldMapping/TextFieldMapping.php +++ b/src/FieldMapping/TextFieldMapping.php @@ -4,13 +4,20 @@ namespace Drupal\fillpdf\FieldMapping; use Drupal\fillpdf\FieldMapping; +/** + * Represents a mapping between a PDF text field and a merge value. + * + * TextFieldMapping objects are immutable; replace the value by calling the + * constructor again if the value needs to change. + */ final class TextFieldMapping extends FieldMapping { /** - * @var string + * Constructs an TextFieldMapping object. + * + * @param string $data + * String containing the text data. */ - protected $data; - public function __construct($data) { // Ensure data is a string. parent::__construct((string) $data); @@ -19,7 +26,7 @@ final class TextFieldMapping extends FieldMapping { /** * @return string */ - public function getData() { + public function __toString() { return parent::getData(); } diff --git a/src/FillPdfBackendManager.php b/src/FillPdfBackendManager.php index 8bb0f49..1eb68cb 100644 --- a/src/FillPdfBackendManager.php +++ b/src/FillPdfBackendManager.php @@ -46,6 +46,9 @@ class FillPdfBackendManager extends DefaultPluginManager { if (!isset($definition['weight'])) { $definitions[$id]['weight'] = 0; } + if (!isset($definition['api'])) { + $definitions[$id]['api'] = 4; + } } uasort($definitions, function ($a, $b) { diff --git a/src/FillPdfBackendPluginInterface.php b/src/FillPdfBackendPluginInterface.php index af5e0e4..87ca0bc 100644 --- a/src/FillPdfBackendPluginInterface.php +++ b/src/FillPdfBackendPluginInterface.php @@ -33,23 +33,8 @@ interface FillPdfBackendPluginInterface { * @param \Drupal\fillpdf\FillPdfFormInterface $fillpdf_form * The FillPdfForm referencing the file whose field values are going to be * populated. - * @param array $field_mapping - * An array of fields mapping PDF field keys to the values with which they - * should be replaced. Example array: - * @code - * [ - * 'values' => [ - * 'Field 1' => 'value', - * 'Checkbox Field' => 'On', - * ], - * 'images' => [ - * 'Image Field 1' => [ - * 'data' => base64_encode($file_data), - * 'filenamehash' => md5($image_path_info['filename']) . '.' . $image_path_info['extension'], - * ], - * ], - * ] - * @endcode + * @param \Drupal\fillpdf\FieldMapping[] $field_mappings + * An array of field mapping objects. * @param array $context * The request context as returned by * FillPdfLinkManipulatorInterface::parseLink(). @@ -60,6 +45,6 @@ interface FillPdfBackendPluginInterface { * * @see \Drupal\fillpdf\FillPdfLinkManipulatorInterface::parseLink() */ - public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mapping, array $context); + public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mappings, array $context); } diff --git a/src/FillPdfContextManagerInterface.php b/src/FillPdfContextManagerInterface.php index 4548130..f329e97 100644 --- a/src/FillPdfContextManagerInterface.php +++ b/src/FillPdfContextManagerInterface.php @@ -19,9 +19,9 @@ interface FillPdfContextManagerInterface { * The FillPDF request context as returned by * FillPdfLinkManipulatorInterface::parseLink(). * - * @return \Drupal\Core\Entity\EntityInterface[] - * An array of entity objects indexed by their IDs. Returns an empty array - * if no matching entities are found. + * @return \Drupal\Core\Entity\EntityInterface[][] + * Multidimensional array of entities, keyed by ID and grouped by entity + * type. Returns an empty array if no matching entities are found. * * @see \Drupal\fillpdf\FillPdfLinkManipulatorInterface::parseLink() */ diff --git a/src/OutputHandler.php b/src/OutputHandler.php index 9428cf7..c471908 100644 --- a/src/OutputHandler.php +++ b/src/OutputHandler.php @@ -110,7 +110,7 @@ class OutputHandler implements OutputHandlerInterface { * The normalized URI */ protected function processDestinationPath($destination_path, array $entities, $scheme = 'public') { - $destination_path = $this->tokenResolver->replace($destination_path, $entities); + $destination_path = (string) $this->tokenResolver->replace($destination_path, $entities, ['content' => 'text']); return FillPdf::buildFileUri($scheme, $destination_path); } diff --git a/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php b/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php index 3eb58a7..2dccf93 100644 --- a/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php +++ b/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php @@ -6,10 +6,13 @@ use Drupal\Core\Plugin\PluginBase; use Drupal\file\Entity\File; use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfFormInterface; +use Drupal\fillpdf\FieldMapping\ImageFieldMapping; +use Drupal\fillpdf\FieldMapping\TextFieldMapping; /** * @Plugin( * id = "fillpdf_service", + * api = 4.5, * label = @Translation("FillPDF Service"), * description = @Translation( * "No technical prerequisites. Sign up for <a href=':url'>FillPDF Service</a>.", @@ -80,25 +83,28 @@ class FillPdfServiceFillPdfBackend extends PluginBase implements FillPdfBackendP /** * {@inheritdoc} */ - public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mapping, array $context) { + public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mappings, array $context) { /** @var \Drupal\file\FileInterface $original_file */ $original_file = File::load($fillpdf_form->file->target_id); $original_pdf = file_get_contents($original_file->getFileUri()); $api_key = $this->configuration['fillpdf_service_api_key']; - // Anonymize image data from the fields array; we should not send the real - // filename to FillPDF Service. We do this in the specific backend because - // other plugin types may need the filename on the local system. - foreach ($field_mapping['fields'] as $field_name => &$field) { - if (!empty($field_mapping['images'][$field_name])) { - // TODO: TEST. - $field_path_info = pathinfo($field_mapping['images'][$field_name]['filenamehash']); - $field = '{image}' . md5($field_path_info['filename']) . '.' . $field_path_info['extension']; + $fields = $images = []; + foreach ($field_mappings as $pdf_key => $mapping) { + if ($mapping instanceof TextFieldMapping) { + $fields[$pdf_key] = (string) $mapping; + } + elseif ($mapping instanceof ImageFieldMapping) { + // Anonymize image data from the fields array; we should not send the + // real filename to FillPDF Service. We do this in the specific backend + // because other plugin types may need the filename on the local system. + $field_path_info = pathinfo($mapping->getUri()); + $fields[$pdf_key] = '{image}' . md5($field_path_info['filename']) . '.' . $field_path_info['extension']; + $images[$pdf_key] = $mapping->getData(); } } - unset($field); - $result = $this->xmlRpcRequest('merge_pdf_v3', base64_encode($original_pdf), $field_mapping['fields'], $api_key, $context['flatten'], $field_mapping['images']); + $result = $this->xmlRpcRequest('merge_pdf_v3', base64_encode($original_pdf), $fields, $api_key, $context['flatten'], $images); if ($result->error === FALSE && $result->data) { $populated_pdf = base64_decode($result->data); diff --git a/src/Plugin/FillPdfBackend/LocalFillPdfBackend.php b/src/Plugin/FillPdfBackend/LocalFillPdfBackend.php index 614d73e..9926965 100644 --- a/src/Plugin/FillPdfBackend/LocalFillPdfBackend.php +++ b/src/Plugin/FillPdfBackend/LocalFillPdfBackend.php @@ -8,11 +8,14 @@ use Drupal\Core\Plugin\PluginBase; use Drupal\file\Entity\File; use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfFormInterface; +use Drupal\fillpdf\FieldMapping\ImageFieldMapping; +use Drupal\fillpdf\FieldMapping\TextFieldMapping; use Symfony\Component\DependencyInjection\ContainerInterface; /** * @Plugin( * id = "local", + * api = 4.5, * label = @Translation("Local PHP/Java-Bridge"), * description = @Translation("Legacy. Use FillPDF LocalServer instead."), * weight = 10 @@ -80,25 +83,22 @@ class LocalFillPdfBackend extends PluginBase implements FillPdfBackendPluginInte /** * {@inheritdoc} */ - public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mapping, array $context) { + public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mappings, array $context) { /** @var \Drupal\file\FileInterface $original_file */ $original_file = File::load($fillpdf_form->file->target_id); $pdf_data = file_get_contents($original_file->getFileUri()); - $fields = $field_mapping['fields']; $require = drupal_get_path('module', 'fillpdf') . '/lib/JavaBridge/java/Java.inc'; require_once DRUPAL_ROOT . '/' . $require; try { $fillpdf = new \java('com.ocdevel.FillpdfService', base64_encode($pdf_data), 'bytes'); - foreach ($fields as $key => $field) { - if (substr($field, 0, 7) == '{image}') { - // Remove {image} marker. - $image_filepath = substr($field, 7); - $image_realpath = $this->fileSystem->realpath($image_filepath); - $fillpdf->image($key, $image_realpath, 'file'); + + foreach ($field_mappings as $pdf_key => $mapping) { + if ($mapping instanceof TextFieldMapping) { + $fillpdf->text($pdf_key, (string) $mapping); } - else { - $fillpdf->text($key, $field); + elseif ($mapping instanceof ImageFieldMapping) { + $fillpdf->image($pdf_key, $this->fileSystem->realpath($mapping->getUri()), 'file'); } } } diff --git a/src/Plugin/FillPdfBackend/LocalService.php b/src/Plugin/FillPdfBackend/LocalService.php index 70f3761..271c467 100644 --- a/src/Plugin/FillPdfBackend/LocalService.php +++ b/src/Plugin/FillPdfBackend/LocalService.php @@ -2,12 +2,9 @@ namespace Drupal\fillpdf\Plugin\FillPdfBackend; -use Drupal\Core\File\FileSystem; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\file\Entity\File; -use Drupal\fillpdf\FieldMapping\ImageFieldMapping; -use Drupal\fillpdf\FieldMapping\TextFieldMapping; use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\Plugin\BackendServiceManager; @@ -16,6 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * @Plugin( * id = "local_service", + * api = 4.5, * label = @Translation("FillPDF LocalServer"), * description = @Translation("Network-accessible, self-installed PDF API. You will need a VPS or dedicated server."), * weight = 5 @@ -23,13 +21,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ContainerFactoryPluginInterface { - /** - * The file system. - * - * @var \Drupal\Core\File\FileSystem - */ - protected $fileSystem; - /** * The FillPDF backend service manager. * @@ -48,13 +39,10 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, * The plugin implementation definition. * @param \Drupal\fillpdf\Plugin\BackendServiceManager $backend_service_manager * The backend service manager. - * @param \Drupal\Core\File\FileSystem $file_system - * The file system. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, BackendServiceManager $backend_service_manager, FileSystem $file_system) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, BackendServiceManager $backend_service_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->backendServiceManager = $backend_service_manager; - $this->fileSystem = $file_system; } /** @@ -65,8 +53,7 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, $configuration, $plugin_id, $plugin_definition, - $container->get('plugin.manager.fillpdf_backend_service'), - $container->get('file_system') + $container->get('plugin.manager.fillpdf_backend_service') ); } @@ -74,43 +61,27 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, * {@inheritdoc} */ public function parse(FillPdfFormInterface $fillpdf_form) { - /** @var \Drupal\file\FileInterface $file */ - $file = File::load($fillpdf_form->file->target_id); - $pdf = file_get_contents($file->getFileUri()); + /** @var \Drupal\file\FileInterface $template_file */ + $template_file = File::load($fillpdf_form->file->target_id); + $pdf_content = file_get_contents($template_file->getFileUri()); /** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */ $backend_service = $this->backendServiceManager->createInstance($this->pluginId, $this->configuration); - return $backend_service->parse($pdf); + return $backend_service->parse($pdf_content); } /** * {@inheritdoc} */ - public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mapping, array $context) { - /** @var \Drupal\file\FileInterface $original_file */ - $original_file = File::load($fillpdf_form->file->target_id); - $pdf = file_get_contents($original_file->getFileUri()); - - // To use the BackendService, we need to convert the fields into the format - // it expects. - $mapping_objects = []; - foreach ($field_mapping['fields'] as $key => $field) { - if (substr($field, 0, 7) === '{image}') { - // Remove {image} marker. - $image_filepath = substr($field, 7); - $image_realpath = $this->fileSystem->realpath($image_filepath); - $mapping_objects[$key] = new ImageFieldMapping(file_get_contents($image_realpath), pathinfo($image_filepath, PATHINFO_EXTENSION)); - } - else { - $mapping_objects[$key] = new TextFieldMapping($field); - } - } + public function populateWithFieldData(FillPdfFormInterface $fillpdf_form, array $field_mappings, array $context) { + $template_file = File::load($fillpdf_form->file->target_id); + $pdf_content = file_get_contents($template_file->getFileUri()); /** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */ $backend_service = $this->backendServiceManager->createInstance($this->pluginId, $this->configuration); - return $backend_service->merge($pdf, $mapping_objects, $context); + return $backend_service->merge($pdf_content, $field_mappings, $context); } } diff --git a/src/Plugin/FillPdfBackend/PdftkFillPdfBackend.php b/src/Plugin/FillPdfBackend/PdftkFillPdfBackend.php index 935d1b8..3f6de1f 100644 --- a/src/Plugin/FillPdfBackend/PdftkFillPdfBackend.php +++ b/src/Plugin/FillPdfBackend/PdftkFillPdfBackend.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\file\Entity\File; use Drupal\fillpdf\Component\Utility\FillPdf; +use Drupal\fillpdf\FieldMapping\TextFieldMapping; use Drupal\fillpdf\FillPdfAdminFormHelperInterface; use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfFormInterface; @@ -16,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * @Plugin( * id = "pdftk", + * api = 4.5, * label = @Translation("pdftk"), * description = @Translation( * "Locally installed pdftk. You will need a VPS or a dedicated server to install pdftk, see <a href=':url'>documentation</a>.", @@ -155,11 +157,17 @@ class PdftkFillPdfBackend extends PluginBase implements FillPdfBackendPluginInte /** * {@inheritdoc} */ - public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $field_mapping, array $context) { + public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $field_mappings, array $context) { /** @var \Drupal\file\FileInterface $original_file */ $original_file = File::load($pdf_form->file->target_id); $filename = $original_file->getFileUri(); - $fields = $field_mapping['fields']; + + $fields = []; + foreach ($field_mappings as $pdf_key => $mapping) { + if ($mapping instanceof TextFieldMapping) { + $fields[$pdf_key] = (string) $mapping; + } + } module_load_include('inc', 'fillpdf', 'xfdf'); $xfdf_name = $filename . '.xfdf'; diff --git a/src/TokenResolver.php b/src/TokenResolver.php index 9be46cb..e789c61 100644 --- a/src/TokenResolver.php +++ b/src/TokenResolver.php @@ -2,8 +2,16 @@ namespace Drupal\fillpdf; -use Drupal\Core\Utility\Token; use Drupal\Component\Render\PlainTextOutput; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Utility\Token; +use Drupal\file\Entity\File; +use Drupal\image\Plugin\Field\FieldType\ImageItem; +use Drupal\fillpdf\FieldMapping\ImageFieldMapping; +use Drupal\fillpdf\FieldMapping\TextFieldMapping; use Drupal\token\TokenEntityMapperInterface; /** @@ -27,42 +35,178 @@ class TokenResolver implements TokenResolverInterface { */ protected $tokenEntityMapper; + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + /** * Constructs a TokenResolver object. * - * @param \Drupal\Core\Utility\Token $token + * @param \Drupal\Core\Utility\Token $token_service * The token service. * @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper * The token entity mapper. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(Token $token_service, TokenEntityMapperInterface $token_entity_mapper) { + public function __construct(Token $token_service, TokenEntityMapperInterface $token_entity_mapper, ModuleHandlerInterface $module_handler) { $this->tokenService = $token_service; $this->tokenEntityMapper = $token_entity_mapper; + $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ - public function replace($original, array $entities) { - // Whichever entity matches the token last wins. - $replaced_string = $original; - - foreach ($entities as $entity_type => $entity_objects) { - // Our rule is "last value wins." So use the last entities's values first. - $seititne = array_reverse($entity_objects); // Get it? - - foreach ($seititne as $entity_id => $entity) { - $replaced_string = $this->tokenService->replace($replaced_string, [ - $entity_type => $entity, - ]); + public function replace($text, array $data = [], array $options = []) { + // Initialize with defaults. + $options += [ + 'content' => '', + 'replacements' => [], + 'prefix' => '', + 'suffix' => '', + ]; + + $tokens = $this->tokenService->scan($text); + if (empty($tokens)) { + return new TextFieldMapping(PlainTextOutput::renderFromHtml($text)); + } + + // Content may be marked as either 'text', 'image' or '' (= unknown). + // @todo: Revisit when enforcing FillPdfFields to be one or the other. + // See: https://www.drupal.org/project/fillpdf/issues/3049368 + $maybe_image = ($options['content'] !== 'text'); + $maybe_text = ($options['content'] !== 'image'); + + // Loop through the token types. + $bubbleable_metadata = new BubbleableMetadata(); + $replacements = []; + foreach ($tokens as $token_type => $type_tokens) { + $token_entity_type = $this->tokenEntityMapper->getEntityTypeForTokenType($token_type, FALSE); + if ($token_entity_type && isset($data[$token_entity_type])) { + // At least one provided entity matches this token type. If there's + // more than one entity of this type, make sure the last one matching + // this token wins. + foreach (array_reverse($data[$token_entity_type]) as $entity) { + // Only fieldable entities may supply image tokens. + if ($maybe_image && $entity instanceof FieldableEntityInterface) { + if ($token_entity_type === 'webform_submission' && $this->moduleHandler->moduleExists('webform')) { + $image_mapping = static::parseImageWebformElementTokens(array_keys($type_tokens), $entity); + } + elseif ($this->moduleHandler->moduleExists('image')) { + $image_mapping = static::parseImageFieldTokens(array_keys($type_tokens), $entity); + } + if (!empty($image_mapping)) { + // Early return if we matched an image token. + return $image_mapping; + } + } + if ($maybe_text) { + $replacements += $this->tokenService->generate($token_type, $type_tokens, [$token_type => $entity], $options, $bubbleable_metadata); + } + } } + elseif ($maybe_text) { + // None of the provided entities matches this token type. It may however + // still be a global token. + $replacements += $this->tokenService->generate($token_type, $type_tokens, [], $options, $bubbleable_metadata); + } + // Clear any unresolved tokens of this type from the string. + $replacements += array_fill_keys($tokens[$token_type], ''); + } + + // Apply token replacements. + $resolved_string = str_replace(array_keys($replacements), array_values($replacements), $text); + + // Replace <br /> occurrences with newlines. + $resolved_string = preg_replace('|<br />|', ' +', $resolved_string); + + // Apply transformation replacements. + if (isset($options['replacements'][$resolved_string])) { + $resolved_string = $options['replacements'][$resolved_string]; } - // The entities might not have had values for all the tokens in the pattern. - // Ensure that any remaining tokens are cleared from the string so they - // don't get sent to the PDF. - $clean_replaced_string = $this->tokenService->replace($replaced_string, [], ['clear' => TRUE]); - return PlainTextOutput::renderFromHtml($clean_replaced_string); + // Apply prefix and suffix, unless empty. + if (!empty($resolved_string)) { + $resolved_string = $options['prefix'] . $resolved_string . $options['suffix']; + } + + return new TextFieldMapping(PlainTextOutput::renderFromHtml($resolved_string)); + } + + /** + * Scans a potential webform image element token. + * + * This is only called if webform module is installed and the backend supports + * image stamping. + * + * @param string[] $tokens + * List of non-fully qualified webform_submission tokens. These may be + * image element tokens such as 'values:image' or other tokens. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * Webform submission entity. + * + * @return \Drupal\fillpdf\FieldMapping\ImageFieldMapping|null + * An ImageFieldMapping, or NULL if the tokens were no image element tokens. + */ + protected static function parseImageWebformElementTokens(array $tokens, ContentEntityInterface $entity) { + // Get all non-empty elements. + /** @var \Drupal\webform\WebformSubmissionInterface $entity */ + $elements = $entity->getWebform()->getElementsInitializedFlattenedAndHasValue(); + + // Loop through the tokens, starting with the last one. + foreach (array_reverse($tokens) as $token) { + $name = strtr($token, ['values:' => '']); + if (array_key_exists($name, $elements) && isset($elements[$name]['#type']) && $elements[$name]['#type'] === 'webform_image_file') { + $file = File::load($entity->getElementData($name)); + if ($file) { + $uri = $file->getFileUri(); + return new ImageFieldMapping(file_get_contents($uri), NULL, $uri); + } + } + } + } + + /** + * Scans a potential image field token. + * + * This is only called if image module is installed and the backend supports + * image stamping. + * + * @param string[] $tokens + * List of non-fully qualified tokens for a given entity type. These may be + * image field tokens such as 'field_image' or 'field_image:thumbnail' or + * other tokens such as 'field_name'. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * Fieldable entity. + * + * @return \Drupal\fillpdf\FieldMapping\ImageFieldMapping|null + * An ImageFieldMapping, or NULL if the tokens were no image field tokens. + */ + protected static function parseImageFieldTokens(array $tokens, FieldableEntityInterface $entity) { + // Loop through the tokens, starting with the last one. + foreach (array_reverse($tokens) as $token) { + // Explode token into its field_name and property parts. + list($field_name, $property) = array_pad(explode(':', $token), 2, ''); + + if (!$entity->hasField($field_name)) { + continue; + } + + $item = $entity->get($field_name)->first(); + if (!empty($item) && $item instanceof ImageItem) { + $value = $item->getValue(); + $file = File::load($value['target_id']); + if ($file) { + $uri = $file->getFileUri(); + return new ImageFieldMapping(file_get_contents($uri), NULL, $uri); + } + } + } } /** diff --git a/src/TokenResolverInterface.php b/src/TokenResolverInterface.php index d581ba4..46bc92e 100644 --- a/src/TokenResolverInterface.php +++ b/src/TokenResolverInterface.php @@ -12,20 +12,29 @@ interface TokenResolverInterface { /** * Replaces all tokens in a given string with appropriate values. * - * @param string $original - * The string containing the tokens to replace. - * @param \Drupal\Core\Entity\EntityInterface[][] $entities - * Multidimensional array of entities, keyed by entity ID and grouped by - * entity type. + * This is basically a replacement for \Drupal\Core\Utility\Token::replace(), + * only that it resolves image tokens, applies form and field replacements + * after token replacement, and returns FieldMapping objects. * - * @return string - * The passed-in string after replacing all possible tokens. The default - * implementation of this interface removes any non-matched tokens. + * @param string $text + * An plain text string containing replaceable tokens. + * @param \Drupal\Core\Entity\EntityInterface[][] $data + * (optional) Multidimensional array of entities, keyed by entity ID and + * grouped by entity type. + * @param array $options + * (optional) A keyed array of settings and flags to control the token + * replacement process. Supported options are: + * - langcode: A language code to be used when generating locale-sensitive + * tokens. + * - callback: A callback function that will be used to post-process the + * array of token replacements after they are generated. + * + * @return \Drupal\fillpdf\FieldMapping + * An instance of a FieldMapping. * * @see \Drupal\Core\Utility\Token::replace() - * @see TokenResolver */ - public function replace($original, array $entities); + public function replace($text, array $data = [], array $options = []); /** * Returns the token service. -- GitLab