Skip to content
Snippets Groups Projects
Commit 9b50cda0 authored by Bernd Oliver Suenderhauf's avatar Bernd Oliver Suenderhauf
Browse files

Issue #3048947 by Pancho, wizonesolutions: Refactor HandlePdfController::populatePdf()

parent 7dfeef1c
No related branches found
No related tags found
No related merge requests found
Showing
with 374 additions and 270 deletions
...@@ -35,7 +35,7 @@ services: ...@@ -35,7 +35,7 @@ services:
fillpdf.token_resolver: fillpdf.token_resolver:
class: Drupal\fillpdf\TokenResolver class: Drupal\fillpdf\TokenResolver
arguments: ["@token", "@token.entity_mapper"] arguments: ["@token", "@token.entity_mapper", "@module_handler"]
fillpdf.entity_helper: fillpdf.entity_helper:
class: Drupal\fillpdf\EntityHelper class: Drupal\fillpdf\EntityHelper
......
...@@ -2,23 +2,24 @@ ...@@ -2,23 +2,24 @@
namespace Drupal\fillpdf\Controller; namespace Drupal\fillpdf\Controller;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Link;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\StreamWrapper\StreamWrapperInterface; 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\FillPdfBackendManager;
use Drupal\fillpdf\FillPdfContextManagerInterface; use Drupal\fillpdf\FillPdfContextManagerInterface;
use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfFormInterface;
use Drupal\fillpdf\FillPdfLinkManipulatorInterface; use Drupal\fillpdf\FillPdfLinkManipulatorInterface;
use Drupal\fillpdf\Plugin\FillPdfActionPluginManager;
use Drupal\fillpdf\TokenResolverInterface; 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\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Link;
/** /**
* Class HandlePdfController. * Class HandlePdfController.
...@@ -117,125 +118,61 @@ class HandlePdfController extends ControllerBase { ...@@ -117,125 +118,61 @@ class HandlePdfController extends ControllerBase {
* validation. * validation.
*/ */
public function populatePdf() { public function populatePdf() {
$context = $this->linkManipulator->parseRequest($this->requestStack->getCurrentRequest());
$config = $this->config('fillpdf.settings'); $config = $this->config('fillpdf.settings');
$fillpdf_service = $config->get('backend');
// Load the backend plugin. $context = $this->linkManipulator->parseRequest($this->requestStack->getCurrentRequest());
/** @var \Drupal\fillpdf\FillPdfBackendPluginInterface $backend */ $entities = $this->contextManager->loadEntities($context);
$backend = $this->backendManager->createInstance($fillpdf_service, $config->get());
// @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']); $fillpdf_form = FillPdfForm::load($context['fid']);
$form_replacements = FillPdfMappingHelper::parseReplacements($fillpdf_form->replacements->value); $form_replacements = FillPdfMappingHelper::parseReplacements($fillpdf_form->replacements->value);
$fields = $fillpdf_form->getFormFields();
// Populate entities array based on what user passed in. // Populate mappings array.
$entities = $this->contextManager->loadEntities($context); $field_mappings = [];
foreach ($fillpdf_form->getFormFields() as $pdf_key => $field) {
$field_mapping = [
'fields' => [],
'images' => [],
];
$mapped_fields = &$field_mapping['fields'];
$image_data = &$field_mapping['images'];
foreach ($fields as $pdf_key => $field) {
if ($context['sample']) { if ($context['sample']) {
$mapped_fields[$pdf_key] = $pdf_key; $field_mappings[$pdf_key] = new TextFieldMapping($pdf_key);
} }
else { else {
// Get image fields attached to the entity and derive their token names $options = [];
// based on the entity types we are working with at the moment. // Our pdftk backend doesn't support image stamping, so at least for
$fill_pattern = count($field->value) ? $field->value->value : ''; // this backend we already know which type of content we can expect.
$is_image_token = FALSE; $options['content'] = $config->get('backend') == 'pdftk' ? 'text' : '';
foreach ($entities as $entity_type => $entities_of_that_type) {
$lifo_entities = array_reverse($entities_of_that_type); // Prepare transformations with field-level replacements taking
foreach ($lifo_entities as $entity) { // precedence over form-level replacements.
if (method_exists($entity, 'getFields')) { $options['replacements'] = FillPdfMappingHelper::parseReplacements($field->replacements->value) + $form_replacements;
// Translate entity type into token type.
$token_type = $this->tokenResolver->getEntityMapper()->getTokenTypeForEntityType($entity_type); // Add prefix and suffix.
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $options['prefix'] = $field->prefix->value;
foreach ($entity->getFields() as $field_name => $field_data) { $options['suffix'] = $field->suffix->value;
$field_definition = $field_data->getFieldDefinition();
if ($field_definition->getType() === 'image') { // Resolve tokens.
if ($fill_pattern === "[{$token_type}:{$field_name}]") { $text = count($field->value) ? $field->value->value : '';
// It's a match! $field_mappings[$pdf_key] = $this->tokenResolver->replace($text, $entities, $options);
$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;
}
} }
} }
$title_pattern = $fillpdf_form->title->value;
// Generate the filename of downloaded PDF from title of the PDF set in // Generate the filename of downloaded PDF from title of the PDF set in
// admin/structure/fillpdf/%fid. // 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)) { if (empty($populated_pdf)) {
$this->messenger()->addError($this->t('Merging the FillPDF Form failed.')); $this->messenger()->addError($this->t('Merging the FillPDF Form failed.'));
return new RedirectResponse(Url::fromRoute('<front>')->toString()); return new RedirectResponse(Url::fromRoute('<front>')->toString());
...@@ -254,10 +191,13 @@ class HandlePdfController extends ControllerBase { ...@@ -254,10 +191,13 @@ class HandlePdfController extends ControllerBase {
* The original filename without tokens being replaced. * The original filename without tokens being replaced.
* @param \Drupal\Core\Entity\EntityInterface[] $entities * @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entities to be used for replacing tokens. * An array of entities to be used for replacing tokens.
*
* @return string
* The token-replaced filename.
*/ */
protected function buildFilename($original, array $entities) { protected function buildFilename($original, array $entities) {
// Replace tokens *before* sanitization. // 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 = str_replace(' ', '_', $original);
$output_name = preg_replace('/\.pdf$/i', '', $output_name); $output_name = preg_replace('/\.pdf$/i', '', $output_name);
...@@ -355,44 +295,43 @@ class HandlePdfController extends ControllerBase { ...@@ -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 * @internal
* Image file object. * @todo Remove in 8.x-5.x. Backends have different requirements, so should
* @param string[] $mapped_fields * handle image files themselves.
* Array of mapped fields.
* @param string $pdf_key
* PDF key.
* @param array[] $image_data
* Array of image data, keyed by the PDF key.
*/ */
protected function processImageTokens(FileInterface $image_file, array &$mapped_fields, $pdf_key, array &$image_data) { private static function legacyFieldMapping(array $field_mappings) {
$backend = $this->config('fillpdf.settings')->get('backend'); $legacy_mapping = [];
foreach ($field_mappings as $pdf_key => $mapping) {
// @todo Refactor in 8.x-5.x. Pdftk doesn't support image stamping. if ($mapping instanceof TextFieldMapping) {
if ($backend == 'pdftk') { $legacy_mapping['fields'][$pdf_key] = (string) $mapping->getData();
return; }
} elseif ($mapping instanceof ImageFieldMapping) {
$uri = (string) $mapping->getUri();
$image_path = $image_file->getFileUri(); if ($uri) {
$mapped_fields[$pdf_key] = "{image}{$image_path}"; $legacy_mapping['fields'][$pdf_key] = "{image}{$uri}";
$image_path_info = pathinfo($uri);
// @todo Refactor in 8.x-5.x. Local and LocalService backends handle image $legacy_mapping['images'][$pdf_key] = [
// files themselves. So this only remains in place for FillPdfService 'data' => base64_encode($mapping->getData()),
// and possible third-party backend plugins. 'filenamehash' => md5($image_path_info['filename']) . '.' . $image_path_info['extension'],
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'],
];
} }
return $legacy_mapping;
} }
} }
...@@ -13,16 +13,28 @@ namespace Drupal\fillpdf; ...@@ -13,16 +13,28 @@ namespace Drupal\fillpdf;
abstract class FieldMapping { abstract class FieldMapping {
/** /**
* @var mixed
*
* The primary value of the mapping. * The primary value of the mapping.
*
* @var string
*/ */
protected $data; protected $data;
/**
* Constructs a FieldMapping object.
*
* @param string $data
* String containing the data.
*/
public function __construct($data) { public function __construct($data) {
$this->data = $data; $this->data = $data;
} }
/**
* Returns the data.
*
* @return string
* String containing the data.
*/
public function getData() { public function getData() {
return $this->data; return $this->data;
} }
......
...@@ -21,6 +21,13 @@ class ImageFieldMapping extends FieldMapping { ...@@ -21,6 +21,13 @@ class ImageFieldMapping extends FieldMapping {
*/ */
protected $extension; protected $extension;
/**
* The image file's URI.
*
* @var string
*/
protected $uri;
/** /**
* Constructs an ImageFieldMapping object. * Constructs an ImageFieldMapping object.
* *
...@@ -31,18 +38,21 @@ class ImageFieldMapping extends FieldMapping { ...@@ -31,18 +38,21 @@ class ImageFieldMapping extends FieldMapping {
* (optional) The original extension corresponding to the image data. If the * (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 * backend doesn't need to know the extension and you don't want extensions
* to be checked, you can leave it blank. * to be checked, you can leave it blank.
* @param string $uri
* (optional) URI of the image.
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* If the extension isn't one of 'jpg', 'png', or 'gif'. * 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); parent::__construct($data);
if (isset($extension) && !in_array($extension, ['jpg', 'png', 'gif'])) { if (isset($extension) && !in_array($extension, ['jpg', 'png', 'gif'])) {
throw new \InvalidArgumentException('Extension must be one of: jpg, png, gif.'); throw new \InvalidArgumentException('Extension must be one of: jpg, png, gif.');
} }
$this->extension = $extension; $this->extension = $extension;
$this->uri = $uri;
} }
/** /**
...@@ -55,4 +65,14 @@ class ImageFieldMapping extends FieldMapping { ...@@ -55,4 +65,14 @@ class ImageFieldMapping extends FieldMapping {
return $this->extension; return $this->extension;
} }
/**
* Gets the image file's URI.
*
* @return string
* The file's URI.
*/
public function getUri() {
return $this->uri;
}
} }
...@@ -4,13 +4,20 @@ namespace Drupal\fillpdf\FieldMapping; ...@@ -4,13 +4,20 @@ namespace Drupal\fillpdf\FieldMapping;
use 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 { 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) { public function __construct($data) {
// Ensure data is a string. // Ensure data is a string.
parent::__construct((string) $data); parent::__construct((string) $data);
...@@ -19,7 +26,7 @@ final class TextFieldMapping extends FieldMapping { ...@@ -19,7 +26,7 @@ final class TextFieldMapping extends FieldMapping {
/** /**
* @return string * @return string
*/ */
public function getData() { public function __toString() {
return parent::getData(); return parent::getData();
} }
......
...@@ -46,6 +46,9 @@ class FillPdfBackendManager extends DefaultPluginManager { ...@@ -46,6 +46,9 @@ class FillPdfBackendManager extends DefaultPluginManager {
if (!isset($definition['weight'])) { if (!isset($definition['weight'])) {
$definitions[$id]['weight'] = 0; $definitions[$id]['weight'] = 0;
} }
if (!isset($definition['api'])) {
$definitions[$id]['api'] = 4;
}
} }
uasort($definitions, function ($a, $b) { uasort($definitions, function ($a, $b) {
......
...@@ -33,23 +33,8 @@ interface FillPdfBackendPluginInterface { ...@@ -33,23 +33,8 @@ interface FillPdfBackendPluginInterface {
* @param \Drupal\fillpdf\FillPdfFormInterface $fillpdf_form * @param \Drupal\fillpdf\FillPdfFormInterface $fillpdf_form
* The FillPdfForm referencing the file whose field values are going to be * The FillPdfForm referencing the file whose field values are going to be
* populated. * populated.
* @param array $field_mapping * @param \Drupal\fillpdf\FieldMapping[] $field_mappings
* An array of fields mapping PDF field keys to the values with which they * An array of field mapping objects.
* 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 array $context * @param array $context
* The request context as returned by * The request context as returned by
* FillPdfLinkManipulatorInterface::parseLink(). * FillPdfLinkManipulatorInterface::parseLink().
...@@ -60,6 +45,6 @@ interface FillPdfBackendPluginInterface { ...@@ -60,6 +45,6 @@ interface FillPdfBackendPluginInterface {
* *
* @see \Drupal\fillpdf\FillPdfLinkManipulatorInterface::parseLink() * @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);
} }
...@@ -19,9 +19,9 @@ interface FillPdfContextManagerInterface { ...@@ -19,9 +19,9 @@ interface FillPdfContextManagerInterface {
* The FillPDF request context as returned by * The FillPDF request context as returned by
* FillPdfLinkManipulatorInterface::parseLink(). * FillPdfLinkManipulatorInterface::parseLink().
* *
* @return \Drupal\Core\Entity\EntityInterface[] * @return \Drupal\Core\Entity\EntityInterface[][]
* An array of entity objects indexed by their IDs. Returns an empty array * Multidimensional array of entities, keyed by ID and grouped by entity
* if no matching entities are found. * type. Returns an empty array if no matching entities are found.
* *
* @see \Drupal\fillpdf\FillPdfLinkManipulatorInterface::parseLink() * @see \Drupal\fillpdf\FillPdfLinkManipulatorInterface::parseLink()
*/ */
......
...@@ -110,7 +110,7 @@ class OutputHandler implements OutputHandlerInterface { ...@@ -110,7 +110,7 @@ class OutputHandler implements OutputHandlerInterface {
* The normalized URI * The normalized URI
*/ */
protected function processDestinationPath($destination_path, array $entities, $scheme = 'public') { 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); return FillPdf::buildFileUri($scheme, $destination_path);
} }
......
...@@ -6,10 +6,13 @@ use Drupal\Core\Plugin\PluginBase; ...@@ -6,10 +6,13 @@ use Drupal\Core\Plugin\PluginBase;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfBackendPluginInterface;
use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfFormInterface;
use Drupal\fillpdf\FieldMapping\ImageFieldMapping;
use Drupal\fillpdf\FieldMapping\TextFieldMapping;
/** /**
* @Plugin( * @Plugin(
* id = "fillpdf_service", * id = "fillpdf_service",
* api = 4.5,
* label = @Translation("FillPDF Service"), * label = @Translation("FillPDF Service"),
* description = @Translation( * description = @Translation(
* "No technical prerequisites. Sign up for <a href=':url'>FillPDF Service</a>.", * "No technical prerequisites. Sign up for <a href=':url'>FillPDF Service</a>.",
...@@ -80,25 +83,28 @@ class FillPdfServiceFillPdfBackend extends PluginBase implements FillPdfBackendP ...@@ -80,25 +83,28 @@ class FillPdfServiceFillPdfBackend extends PluginBase implements FillPdfBackendP
/** /**
* {@inheritdoc} * {@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 */ /** @var \Drupal\file\FileInterface $original_file */
$original_file = File::load($fillpdf_form->file->target_id); $original_file = File::load($fillpdf_form->file->target_id);
$original_pdf = file_get_contents($original_file->getFileUri()); $original_pdf = file_get_contents($original_file->getFileUri());
$api_key = $this->configuration['fillpdf_service_api_key']; $api_key = $this->configuration['fillpdf_service_api_key'];
// Anonymize image data from the fields array; we should not send the real $fields = $images = [];
// filename to FillPDF Service. We do this in the specific backend because foreach ($field_mappings as $pdf_key => $mapping) {
// other plugin types may need the filename on the local system. if ($mapping instanceof TextFieldMapping) {
foreach ($field_mapping['fields'] as $field_name => &$field) { $fields[$pdf_key] = (string) $mapping;
if (!empty($field_mapping['images'][$field_name])) { }
// TODO: TEST. elseif ($mapping instanceof ImageFieldMapping) {
$field_path_info = pathinfo($field_mapping['images'][$field_name]['filenamehash']); // Anonymize image data from the fields array; we should not send the
$field = '{image}' . md5($field_path_info['filename']) . '.' . $field_path_info['extension']; // 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) { if ($result->error === FALSE && $result->data) {
$populated_pdf = base64_decode($result->data); $populated_pdf = base64_decode($result->data);
......
...@@ -8,11 +8,14 @@ use Drupal\Core\Plugin\PluginBase; ...@@ -8,11 +8,14 @@ use Drupal\Core\Plugin\PluginBase;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfBackendPluginInterface;
use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfFormInterface;
use Drupal\fillpdf\FieldMapping\ImageFieldMapping;
use Drupal\fillpdf\FieldMapping\TextFieldMapping;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* @Plugin( * @Plugin(
* id = "local", * id = "local",
* api = 4.5,
* label = @Translation("Local PHP/Java-Bridge"), * label = @Translation("Local PHP/Java-Bridge"),
* description = @Translation("Legacy. Use FillPDF LocalServer instead."), * description = @Translation("Legacy. Use FillPDF LocalServer instead."),
* weight = 10 * weight = 10
...@@ -80,25 +83,22 @@ class LocalFillPdfBackend extends PluginBase implements FillPdfBackendPluginInte ...@@ -80,25 +83,22 @@ class LocalFillPdfBackend extends PluginBase implements FillPdfBackendPluginInte
/** /**
* {@inheritdoc} * {@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 */ /** @var \Drupal\file\FileInterface $original_file */
$original_file = File::load($fillpdf_form->file->target_id); $original_file = File::load($fillpdf_form->file->target_id);
$pdf_data = file_get_contents($original_file->getFileUri()); $pdf_data = file_get_contents($original_file->getFileUri());
$fields = $field_mapping['fields'];
$require = drupal_get_path('module', 'fillpdf') . '/lib/JavaBridge/java/Java.inc'; $require = drupal_get_path('module', 'fillpdf') . '/lib/JavaBridge/java/Java.inc';
require_once DRUPAL_ROOT . '/' . $require; require_once DRUPAL_ROOT . '/' . $require;
try { try {
$fillpdf = new \java('com.ocdevel.FillpdfService', base64_encode($pdf_data), 'bytes'); $fillpdf = new \java('com.ocdevel.FillpdfService', base64_encode($pdf_data), 'bytes');
foreach ($fields as $key => $field) {
if (substr($field, 0, 7) == '{image}') { foreach ($field_mappings as $pdf_key => $mapping) {
// Remove {image} marker. if ($mapping instanceof TextFieldMapping) {
$image_filepath = substr($field, 7); $fillpdf->text($pdf_key, (string) $mapping);
$image_realpath = $this->fileSystem->realpath($image_filepath);
$fillpdf->image($key, $image_realpath, 'file');
} }
else { elseif ($mapping instanceof ImageFieldMapping) {
$fillpdf->text($key, $field); $fillpdf->image($pdf_key, $this->fileSystem->realpath($mapping->getUri()), 'file');
} }
} }
} }
......
...@@ -2,12 +2,9 @@ ...@@ -2,12 +2,9 @@
namespace Drupal\fillpdf\Plugin\FillPdfBackend; namespace Drupal\fillpdf\Plugin\FillPdfBackend;
use Drupal\Core\File\FileSystem;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Plugin\PluginBase;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\fillpdf\FieldMapping\ImageFieldMapping;
use Drupal\fillpdf\FieldMapping\TextFieldMapping;
use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfBackendPluginInterface;
use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfFormInterface;
use Drupal\fillpdf\Plugin\BackendServiceManager; use Drupal\fillpdf\Plugin\BackendServiceManager;
...@@ -16,6 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; ...@@ -16,6 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* @Plugin( * @Plugin(
* id = "local_service", * id = "local_service",
* api = 4.5,
* label = @Translation("FillPDF LocalServer"), * label = @Translation("FillPDF LocalServer"),
* description = @Translation("Network-accessible, self-installed PDF API. You will need a VPS or dedicated server."), * description = @Translation("Network-accessible, self-installed PDF API. You will need a VPS or dedicated server."),
* weight = 5 * weight = 5
...@@ -23,13 +21,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; ...@@ -23,13 +21,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/ */
class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ContainerFactoryPluginInterface { class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ContainerFactoryPluginInterface {
/**
* The file system.
*
* @var \Drupal\Core\File\FileSystem
*/
protected $fileSystem;
/** /**
* The FillPDF backend service manager. * The FillPDF backend service manager.
* *
...@@ -48,13 +39,10 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ...@@ -48,13 +39,10 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface,
* The plugin implementation definition. * The plugin implementation definition.
* @param \Drupal\fillpdf\Plugin\BackendServiceManager $backend_service_manager * @param \Drupal\fillpdf\Plugin\BackendServiceManager $backend_service_manager
* The 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); parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->backendServiceManager = $backend_service_manager; $this->backendServiceManager = $backend_service_manager;
$this->fileSystem = $file_system;
} }
/** /**
...@@ -65,8 +53,7 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ...@@ -65,8 +53,7 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface,
$configuration, $configuration,
$plugin_id, $plugin_id,
$plugin_definition, $plugin_definition,
$container->get('plugin.manager.fillpdf_backend_service'), $container->get('plugin.manager.fillpdf_backend_service')
$container->get('file_system')
); );
} }
...@@ -74,43 +61,27 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface, ...@@ -74,43 +61,27 @@ class LocalService extends PluginBase implements FillPdfBackendPluginInterface,
* {@inheritdoc} * {@inheritdoc}
*/ */
public function parse(FillPdfFormInterface $fillpdf_form) { public function parse(FillPdfFormInterface $fillpdf_form) {
/** @var \Drupal\file\FileInterface $file */ /** @var \Drupal\file\FileInterface $template_file */
$file = File::load($fillpdf_form->file->target_id); $template_file = File::load($fillpdf_form->file->target_id);
$pdf = file_get_contents($file->getFileUri()); $pdf_content = file_get_contents($template_file->getFileUri());
/** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */ /** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */
$backend_service = $this->backendServiceManager->createInstance($this->pluginId, $this->configuration); $backend_service = $this->backendServiceManager->createInstance($this->pluginId, $this->configuration);
return $backend_service->parse($pdf); return $backend_service->parse($pdf_content);
} }
/** /**
* {@inheritdoc} * {@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 */ $template_file = File::load($fillpdf_form->file->target_id);
$original_file = File::load($fillpdf_form->file->target_id); $pdf_content = file_get_contents($template_file->getFileUri());
$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);
}
}
/** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */ /** @var \Drupal\fillpdf\Plugin\BackendServiceInterface $backend_service */
$backend_service = $this->backendServiceManager->createInstance($this->pluginId, $this->configuration); $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);
} }
} }
...@@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; ...@@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Plugin\PluginBase;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\fillpdf\Component\Utility\FillPdf; use Drupal\fillpdf\Component\Utility\FillPdf;
use Drupal\fillpdf\FieldMapping\TextFieldMapping;
use Drupal\fillpdf\FillPdfAdminFormHelperInterface; use Drupal\fillpdf\FillPdfAdminFormHelperInterface;
use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfBackendPluginInterface;
use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfFormInterface;
...@@ -16,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; ...@@ -16,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* @Plugin( * @Plugin(
* id = "pdftk", * id = "pdftk",
* api = 4.5,
* label = @Translation("pdftk"), * label = @Translation("pdftk"),
* description = @Translation( * description = @Translation(
* "Locally installed pdftk. You will need a VPS or a dedicated server to install pdftk, see <a href=':url'>documentation</a>.", * "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 ...@@ -155,11 +157,17 @@ class PdftkFillPdfBackend extends PluginBase implements FillPdfBackendPluginInte
/** /**
* {@inheritdoc} * {@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 */ /** @var \Drupal\file\FileInterface $original_file */
$original_file = File::load($pdf_form->file->target_id); $original_file = File::load($pdf_form->file->target_id);
$filename = $original_file->getFileUri(); $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'); module_load_include('inc', 'fillpdf', 'xfdf');
$xfdf_name = $filename . '.xfdf'; $xfdf_name = $filename . '.xfdf';
......
...@@ -2,8 +2,16 @@ ...@@ -2,8 +2,16 @@
namespace Drupal\fillpdf; namespace Drupal\fillpdf;
use Drupal\Core\Utility\Token;
use Drupal\Component\Render\PlainTextOutput; 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; use Drupal\token\TokenEntityMapperInterface;
/** /**
...@@ -27,42 +35,178 @@ class TokenResolver implements TokenResolverInterface { ...@@ -27,42 +35,178 @@ class TokenResolver implements TokenResolverInterface {
*/ */
protected $tokenEntityMapper; protected $tokenEntityMapper;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/** /**
* Constructs a TokenResolver object. * Constructs a TokenResolver object.
* *
* @param \Drupal\Core\Utility\Token $token * @param \Drupal\Core\Utility\Token $token_service
* The token service. * The token service.
* @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper * @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper
* The 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->tokenService = $token_service;
$this->tokenEntityMapper = $token_entity_mapper; $this->tokenEntityMapper = $token_entity_mapper;
$this->moduleHandler = $module_handler;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function replace($original, array $entities) { public function replace($text, array $data = [], array $options = []) {
// Whichever entity matches the token last wins. // Initialize with defaults.
$replaced_string = $original; $options += [
'content' => '',
foreach ($entities as $entity_type => $entity_objects) { 'replacements' => [],
// Our rule is "last value wins." So use the last entities's values first. 'prefix' => '',
$seititne = array_reverse($entity_objects); // Get it? 'suffix' => '',
];
foreach ($seititne as $entity_id => $entity) {
$replaced_string = $this->tokenService->replace($replaced_string, [ $tokens = $this->tokenService->scan($text);
$entity_type => $entity, 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. // Apply prefix and suffix, unless empty.
// Ensure that any remaining tokens are cleared from the string so they if (!empty($resolved_string)) {
// don't get sent to the PDF. $resolved_string = $options['prefix'] . $resolved_string . $options['suffix'];
$clean_replaced_string = $this->tokenService->replace($replaced_string, [], ['clear' => TRUE]); }
return PlainTextOutput::renderFromHtml($clean_replaced_string);
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);
}
}
}
} }
/** /**
......
...@@ -12,20 +12,29 @@ interface TokenResolverInterface { ...@@ -12,20 +12,29 @@ interface TokenResolverInterface {
/** /**
* Replaces all tokens in a given string with appropriate values. * Replaces all tokens in a given string with appropriate values.
* *
* @param string $original * This is basically a replacement for \Drupal\Core\Utility\Token::replace(),
* The string containing the tokens to replace. * only that it resolves image tokens, applies form and field replacements
* @param \Drupal\Core\Entity\EntityInterface[][] $entities * after token replacement, and returns FieldMapping objects.
* Multidimensional array of entities, keyed by entity ID and grouped by
* entity type.
* *
* @return string * @param string $text
* The passed-in string after replacing all possible tokens. The default * An plain text string containing replaceable tokens.
* implementation of this interface removes any non-matched 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 \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. * Returns the token service.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment