From 0facb18db3fac7f98bb3729a7d860ed5d23bdb32 Mon Sep 17 00:00:00 2001 From: Kevin Kaland <kevin@wizonesolutions.com> Date: Thu, 20 Nov 2014 15:38:23 +0100 Subject: [PATCH] Convert main route...mostly. I'm getting an error when I actually try to populate/download the PDF. There is probably something wrong with the Request object I'm returning. But this is closer. --- fillpdf.module | 16 +- fillpdf.routing.yml | 2 +- src/Controller/HandlePdfController.php | 161 ++++++++++++++++++ src/FillPdfBackendManager.php | 3 +- src/FillPdfBackendManagerInterface.php | 12 -- src/FillPdfBackendPluginInterface.php | 4 +- src/FillPdfLinkManipulatorInterface.php | 1 + src/Form/FillPdfOverviewForm.php | 37 ++-- .../FillPdfServiceFillPdfBackend.php | 21 ++- src/Service/FillPdfLinkManipulator.php | 56 +++++- 10 files changed, 262 insertions(+), 51 deletions(-) delete mode 100644 src/FillPdfBackendManagerInterface.php diff --git a/fillpdf.module b/fillpdf.module index 7373121..f0fb5e8 100644 --- a/fillpdf.module +++ b/fillpdf.module @@ -390,14 +390,14 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_arr = NULL, $sample = NU $pdf_data = _fillpdf_get_file_contents($fillpdf_info->url, "<front>"); switch (variable_get('fillpdf_service')) { - case 'remote': // use fillpdf-service.com's xmlrpc service (must be registered) - $api_key = variable_get('fillpdf_api_key', '0'); - $result = _fillpdf_xmlrpc_request(DEFAULT_SERVLET_URL, 'merge_pdf_v3', base64_encode($pdf_data), $fields, $api_key, $flatten, $image_data); - if ($result->error == TRUE) { - drupal_goto(); - } // after setting error message - $data = base64_decode($result->data); - break; +// case 'remote': // use fillpdf-service.com's xmlrpc service (must be registered) +// $api_key = variable_get('fillpdf_api_key', '0'); +// $result = _fillpdf_xmlrpc_request(DEFAULT_SERVLET_URL, 'merge_pdf_v3', base64_encode($pdf_data), $fields, $api_key, $flatten, $image_data); +// if ($result->error == TRUE) { +// drupal_goto(); +// } // after setting error message +// $data = base64_decode($result->data); +// break; case 'local': // use local php/java bridge (must have Tomcat & JavaBridge installed on VPS or dedicated $require = drupal_get_path('module', 'fillpdf') . '/lib/JavaBridge/java/Java.inc'; diff --git a/fillpdf.routing.yml b/fillpdf.routing.yml index 0d6ecd1..30790fb 100644 --- a/fillpdf.routing.yml +++ b/fillpdf.routing.yml @@ -21,6 +21,6 @@ fillpdf.forms_admin: fillpdf.populate_pdf: path: '/fillpdf' defaults: - _content: '\Drupal\fillpdf\Controller\HandlePdfController::populatePdf' + _controller: '\Drupal\fillpdf\Controller\HandlePdfController::populatePdf' requirements: _custom_access: '\Drupal\fillpdf\FillPdfAccessController::checkLink' diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php index 9f873e9..c2b1c93 100644 --- a/src/Controller/HandlePdfController.php +++ b/src/Controller/HandlePdfController.php @@ -7,11 +7,172 @@ namespace Drupal\fillpdf\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\fillpdf\Entity\FillPdfForm; +use Drupal\fillpdf\Entity\FillPdfFormField; +use Drupal\fillpdf\FillPdfBackendManager; +use Drupal\fillpdf\FillPdfBackendPluginInterface; +use Drupal\fillpdf\FillPdfFormInterface; +use Drupal\fillpdf\FillPdfLinkManipulatorInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; class HandlePdfController extends ControllerBase { + /** @var FillPdfLinkManipulatorInterface $linkManipulator */ + protected $linkManipulator; + + /** @var FillPdfBackendManager $backendManager */ + protected $backendManager; + + /** @var QueryFactory $entityQuery */ + protected $entityQuery; + + public function __construct(FillPdfLinkManipulatorInterface $link_manipulator, RequestStack $request_stack, FillPdfBackendManager $backend_manager, QueryFactory $entity_query) { + $this->linkManipulator = $link_manipulator; + $this->requestStack = $request_stack; + $this->backendManager = $backend_manager; + $this->entityQuery = $entity_query; + } + + public static function create(ContainerInterface $container) { + return new static( + $container->get('fillpdf.link_manipulator'), + $container->get('request_stack'), + $container->get('plugin.manager.fillpdf_backend'), + $container->get('entity.query') + ); + } + public function populatePdf() { + $context = $this->linkManipulator->parseLink($this->requestStack->getCurrentRequest()); + + $config = $this->config('fillpdf.settings'); + $fillpdf_service = $config->get('fillpdf_service_backend'); + + // Load the backend plugin. + /** @var FillPdfBackendPluginInterface $backend */ + $backend = $this->backendManager->createInstance($fillpdf_service, $config->get()); + + // @todo: Emit event (or call alter hook?) before populating PDF. fillpdf_merge_fields_alter -> should be renamed to fillpdf_populate_fields_alter + // TODO: Extract the PDF parameters, and act accordingly - this is pretty much just calling FillPdfBackendPluginInterface::populateWithFieldData and then reading the FillPdfForm options to determine whether to serve as a file download or fill in the field data. + $fillpdf_form = FillPdfForm::load($context['fid']); + if (!$fillpdf_form) { + drupal_set_message($this->t('FillPDF Form (fid) not found in the system. Please check the value in your FillPDF Link.'), 'error'); + } + + $fields = FillPdfFormField::loadMultiple( + $this->entityQuery->get('fillpdf_form_field') + ->condition('fillpdf_form', $fillpdf_form->id()) + ->execute() + ); + + $populated_pdf = $backend->populateWithFieldData($fillpdf_form, $fields, $context); + + // @todo: When Rules integration ported, emit an event or whatever. + + // TODO: Take the appropriate action on the PDF. + $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, array()); + } + + /** + * @todo: Split this up better. There are a few things happening here. 1) We look at the function arguments to determine the action to take. But there's also the concept of a default action. In that case, we have to look at the $context array. 2) The actual handling code for the action, e.g. transmitting a download and saving a file, optionally redirecting to a file. These should be usable at will, albeit specific to routes. + * + * Figure out what to do with the PDF and do it. + * + * @return Nothing. + * @param \Drupal\fillpdf\Controller\An|\Drupal\fillpdf\FillPdfFormInterface $fillpdf_form An object containing the loaded record from {fillpdf_forms} + * . + * @param $pdf_data A string containing the content of the merged PDF. + * @param array|\Drupal\fillpdf\Controller\An $token_objects An array of objects to be used in replacing tokens. + * Here, specifically, it's for generating the filename of the handled PDF. + * @param \Drupal\fillpdf\Controller\One|string $action One of the following keywords: default, download, save, + * redirect. These correspond to performing the configured action (from + * admin/structure/fillpdf/%), sending the PDF to the user's browser, saving it + * to a file, and saving it to a file and then redirecting the user's browser to + * the saved file. + * @param array|\Drupal\fillpdf\Controller\If $options If set, this function will always end the request by + * sending the filled PDF to the user's browser. + */ + protected function handlePopulatedPdf(FillPdfFormInterface $fillpdf_form, $pdf_data, array $token_objects, $action = 'download', array $options = array()) { + // TODO: Convert rest of this function. + $force_download = FALSE; + if (!empty($option['force_download'])) { + $force_download = TRUE; + } + + if (in_array($action, array('default', 'download', 'save', 'redirect')) === FALSE) { + // Do nothing if the function is called with an invalid action. + return; + } + // Generate the filename of downloaded PDF from title of the PDF set in + // admin/structure/fillpdf/%fid + $output_name = $this->buildFilename($fillpdf_form->title->value, $token_objects); + + // Now that we have a filename, we can build the response we're most likely + // to deliver. Content-Disposition is set further down in the actual + // download case. If it turns out to be a redirect, then we replace this + // variable with a RedirectResponse. + $response = new Response($pdf_data); + + if ($action == 'default') { + // Determine the default action, then re-set $action to that. + if (empty($fillpdf_form->destination_path) === FALSE) { + $action = 'save'; + } + else { + $action = 'download'; + } + } + + // Initialize variable containing whether or not we send the user's browser to + // the saved PDF after saving it (if we are) + $redirect_to_file = FALSE; + + // Get a load of this switch...they all just fall through! + switch ($action) { + case 'redirect': + $redirect_to_file = $fillpdf_form->destination_redirect; + case 'save': + fillpdf_save_to_file($fillpdf_form, $pdf_data, $token_objects, $output_name, !$options, $redirect_to_file); + // FillPDF classic! + case 'download': +// drupal_add_http_header("Pragma", "public"); +// drupal_add_http_header('Expires', 0); +// drupal_add_http_header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0'); +// drupal_add_http_header('Content-type', 'application-download'); +// // This must be strlen(), not drupal_strlen() because the length in bytes, +// // not in characters, is what is needed here. +// drupal_add_http_header('Content-Length', strlen($pdf_data)); +// drupal_add_http_header('Content-disposition', 'attachment; filename="' . $output_name . '"'); +// drupal_add_http_header('Content-Transfer-Encoding', 'binary'); +// echo $pdf_data; +// drupal_exit(); + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $output_name + ); + $response->headers->set('Content-Disposition', $disposition); + break; + } + + return $response; + } + + public function buildFilename($original, array $token_objects) { + // Replace tokens *before* sanitization + if (!empty($token_objects)) { + $original = token_replace($original, $token_objects); + } + + $output_name = str_replace(' ', '_', $original); + $output_name = preg_replace('/\.pdf$/i', '', $output_name); + $output_name = preg_replace('/[^a-zA-Z0-9_.-]+/', '', $output_name) . '.pdf'; + + return $output_name; } } diff --git a/src/FillPdfBackendManager.php b/src/FillPdfBackendManager.php index 3005606..8636c7f 100644 --- a/src/FillPdfBackendManager.php +++ b/src/FillPdfBackendManager.php @@ -12,7 +12,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManager; use Drupal\Core\Plugin\DefaultPluginManager; -class FillPdfBackendManager extends DefaultPluginManager implements FillPdfBackendManagerInterface { +class FillPdfBackendManager extends DefaultPluginManager { /** * Constructs a FillPdfBackendManager object. @@ -28,7 +28,6 @@ class FillPdfBackendManager extends DefaultPluginManager implements FillPdfBacke * The module handler to invoke the alter hook with. */ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { - // @todo: Add a future FillPdfBackendPluginInterface to this? parent::__construct('Plugin/FillPdfBackend', $namespaces, $module_handler, '\Drupal\fillpdf\FillPdfBackendPluginInterface'); $this->alterInfo('fillpdf_backend_info'); $this->setCacheBackend($cache_backend, 'fillpdf_backend_info_plugins'); diff --git a/src/FillPdfBackendManagerInterface.php b/src/FillPdfBackendManagerInterface.php deleted file mode 100644 index db2e14b..0000000 --- a/src/FillPdfBackendManagerInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php -/** - * @file - * Contains \Drupal\fillpdf\FillPdfBackendManagerInterface. - */ - - -namespace Drupal\fillpdf; - -interface FillPdfBackendManagerInterface { - -} diff --git a/src/FillPdfBackendPluginInterface.php b/src/FillPdfBackendPluginInterface.php index 61ee715..23450b7 100644 --- a/src/FillPdfBackendPluginInterface.php +++ b/src/FillPdfBackendPluginInterface.php @@ -38,9 +38,9 @@ interface FillPdfBackendPluginInterface { * 'Field 1' => 'value', * 'Checkbox Field' => 'On', * ); - * @param array $options @todo: Define + * @param array $context @todo: Define * @return string The raw file contents of the new PDF; the caller has to * handle saving or serving the file accordingly. */ - public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $fields, array $options); + public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $fields, array $context); } diff --git a/src/FillPdfLinkManipulatorInterface.php b/src/FillPdfLinkManipulatorInterface.php index 3d86751..26efd4f 100644 --- a/src/FillPdfLinkManipulatorInterface.php +++ b/src/FillPdfLinkManipulatorInterface.php @@ -6,6 +6,7 @@ namespace Drupal\fillpdf; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** diff --git a/src/Form/FillPdfOverviewForm.php b/src/Form/FillPdfOverviewForm.php index 79beb8c..421de2f 100644 --- a/src/Form/FillPdfOverviewForm.php +++ b/src/Form/FillPdfOverviewForm.php @@ -14,7 +14,8 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\fillpdf\Entity\FillPdfForm; -use Drupal\fillpdf\FillPdfBackendManagerInterface; +use Drupal\fillpdf\Entity\FillPdfFormField; +use Drupal\fillpdf\FillPdfBackendManager; use Drupal\fillpdf\FillPdfBackendPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -22,7 +23,7 @@ class FillPdfOverviewForm extends FillPdfAdminFormBase { /** * The backend manager (finds the filling plugin the user selected). * - * @var \Drupal\fillpdf\FillPdfBackendManagerInterface + * @var \Drupal\fillpdf\FillPdfBackendManager */ protected $backendManager; @@ -45,7 +46,7 @@ class FillPdfOverviewForm extends FillPdfAdminFormBase { return 'fillpdf_forms_admin'; } - public function __construct(ModuleHandlerInterface $module_handler, FillPdfBackendManagerInterface $backend_manager, AccountInterface $current_user, QueryFactory $entity_query) { + public function __construct(ModuleHandlerInterface $module_handler, FillPdfBackendManager $backend_manager, AccountInterface $current_user, QueryFactory $entity_query) { parent::__construct(); $this->backendManager = $backend_manager; $this->moduleHandler = $module_handler; @@ -228,27 +229,21 @@ class FillPdfOverviewForm extends FillPdfAdminFormBase { $config = $this->config('fillpdf.settings'); $fillpdf_service = $config->get('fillpdf_service_backend'); - try { - /** @var FillPdfBackendPluginInterface $backend */ - $backend = $this->backendManager->createInstance($fillpdf_service, $config->get()); - } - catch (PluginNotFoundException $exception) { - $backend = NULL; - } + /** @var FillPdfBackendPluginInterface $backend */ + $backend = $this->backendManager->createInstance($fillpdf_service, $config->get()); - if ($backend) { - // Attempt to parse the fields in the PDF. - $fields = $backend->parse($fillpdf_form); + // Attempt to parse the fields in the PDF. + $fields = $backend->parse($fillpdf_form); - // Save the fields that were parsed out (if any). If none were, set a - // warning message telling the user that. - foreach ($fields as $fillpdf_form_field) { - $fillpdf_form_field->save(); - } + // Save the fields that were parsed out (if any). If none were, set a + // warning message telling the user that. + foreach ($fields as $fillpdf_form_field) { + /** @var FillPdfFormField $fillpdf_form_field */ + $fillpdf_form_field->save(); + } - if (empty($fields)) { - drupal_set_message($this->t("No fields detected in PDF. Are you sure it contains editable fields?"), 'warning'); - } + if (empty($fields)) { + drupal_set_message($this->t("No fields detected in PDF. Are you sure it contains editable fields?"), 'warning'); } } } \ No newline at end of file diff --git a/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php b/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php index 0301bdc..ac3f497 100644 --- a/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php +++ b/src/Plugin/FillPdfBackend/FillPdfServiceFillPdfBackend.php @@ -76,8 +76,25 @@ class FillPdfServiceFillPdfBackend implements FillPdfBackendPluginInterface { /** * @inheritdoc */ - public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $fields, array $options) { - // TODO: Implement populateWithFieldData() method. + public function populateWithFieldData(FillPdfFormInterface $pdf_form, array $fields, array $context) { + /** @var FileInterface $original_file */ + $original_file = File::load($pdf_form->file->target_id); + $original_pdf = file_get_contents($original_file->getFileUri()); + + // @todo: Actually write this + $api_key = $this->config['fillpdf_api_key']; + $field_mapping = array(); + // @todo: Image-filling support. Probably both the local and remote plugins + // could extend the same class. + $image_data = array(); + $result = $this->xmlRpcRequest('merge_pdf_v3', base64_encode($original_pdf), $field_mapping, $api_key, $context['flatten'], $image_data); + // @todo: Error handling/exceptions +// if ($result->error == TRUE) { +// drupal_goto(); +// } + + $populated_pdf = base64_decode($result->data); + return $populated_pdf; } diff --git a/src/Service/FillPdfLinkManipulator.php b/src/Service/FillPdfLinkManipulator.php index 75f8e6b..ee900b8 100644 --- a/src/Service/FillPdfLinkManipulator.php +++ b/src/Service/FillPdfLinkManipulator.php @@ -7,22 +7,72 @@ namespace Drupal\fillpdf\Service; use Drupal\fillpdf\FillPdfLinkManipulatorInterface; -use Drupal\fillpdf\Request; +use Symfony\Component\HttpFoundation\Request; class FillPdfLinkManipulator implements FillPdfLinkManipulatorInterface { /** * @param Request $request The request containing the query string to parse. * @return array + * + * @todo: Maybe this should return a FillPdfLinkContext object or something? + * Guess it depends on how much I end up needing to change it. */ public function parseLink(Request $request) { - // TODO: Implement parseLink() method. + $request_context = array( + 'entity_ids' => NULL, + 'fid' => NULL, + 'sample' => NULL, + 'force_download' => FALSE, + 'flatten' => TRUE, + ); + + $request_context['sample'] = $request->get('sample'); // is this just the PDF populated with sample data? + $request_context['fid'] = $request->get('fid'); + + if ($request->get('entity_type')) { + $request_context['entity_type'] = $request->get('entity_type'); + } + + $request_context['entity_ids'] = $entity_ids = array(); + if ($request->get('entity_id') || $request->get('entity_ids')) { + $entity_ids = ($request->get('entity_id') ? array($request->get('entity_id')) : $request->get('entity_ids')); + + // Re-key entity IDs so they can be loaded easily with loadMultiple(). + // If we have type information, add it to the types array, and remove it + // make sure we only store the ID in the entity_ids key. + foreach ($entity_ids as $entity_id) { + $entity_id_parts = explode(':', $entity_id); + + if (count($entity_id_parts) == 2) { + $entity_type = $entity_id_parts[0]; + $entity_id = $entity_id_parts[1]; + } + else { + $entity_type = 'node'; + } + $request_context['entity_ids'] += array( + $entity_type => array(), + ); + + $request_context['entity_ids'][$entity_type][$entity_id] = $entity_id; + } + } + + if ($request->get('download') && (int) $request->get('download') == 1) { + $request_context['force_download'] = TRUE; + } + if ($request->get('flatten') && (int) $request->get('flatten') == 0) { + $request_context['flatten'] = FALSE; + } + + return $request_context; } /** * @param array $parameters * The array of parameters to be converted into a - * URL and query string. + * URL and query string. * @return string */ public function generateLink(array $parameters) { -- GitLab