From 08be2cade1f609ea4ee3428ad58a76b6777e4dc5 Mon Sep 17 00:00:00 2001 From: Kevin Kaland <kevin@wizonesolutions.com> Date: Sat, 17 Oct 2015 20:30:58 +0200 Subject: [PATCH] Issue #2359213: Reimplement downloading as action plugin. --- src/Controller/HandlePdfController.php | 122 ++++++++---------- src/FillPdfBackendPluginInterface.php | 2 + .../FillPdfDownloadAction.php | 39 ++++++ src/Plugin/FillPdfActionPluginBase.php | 22 +++- src/Plugin/FillPdfActionPluginInterface.php | 13 +- 5 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 src/Plugin/FillPdfActionPlugin/FillPdfDownloadAction.php diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php index 2e5501c..4ca0ee8 100644 --- a/src/Controller/HandlePdfController.php +++ b/src/Controller/HandlePdfController.php @@ -17,6 +17,9 @@ use Drupal\fillpdf\FillPdfBackendPluginInterface; use Drupal\fillpdf\FillPdfContextManagerInterface; use Drupal\fillpdf\FillPdfFormInterface; use Drupal\fillpdf\FillPdfLinkManipulatorInterface; +use Drupal\fillpdf\Plugin\FillPdfActionPlugin\FillPdfDownloadAction; +use Drupal\fillpdf\Plugin\FillPdfActionPluginInterface; +use Drupal\fillpdf\Plugin\FillPdfActionPluginManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; @@ -46,11 +49,12 @@ class HandlePdfController extends ControllerBase { /** @var FillPdfContextManagerInterface $contextManager */ protected $contextManager; - public function __construct(FillPdfLinkManipulatorInterface $link_manipulator, FillPdfContextManagerInterface $context_manager, RequestStack $request_stack, FillPdfBackendManager $backend_manager, Token $token, QueryFactory $entity_query, EntityManagerInterface $entity_manager) { + public function __construct(FillPdfLinkManipulatorInterface $link_manipulator, FillPdfContextManagerInterface $context_manager, RequestStack $request_stack, FillPdfBackendManager $backend_manager, FillPdfActionPluginManager $action_manager, Token $token, QueryFactory $entity_query, EntityManagerInterface $entity_manager) { $this->linkManipulator = $link_manipulator; $this->contextManager = $context_manager; $this->requestStack = $request_stack; $this->backendManager = $backend_manager; + $this->actionManager = $action_manager; $this->token = $token; $this->entityQuery = $entity_query; $this->entityManager = $entity_manager; @@ -65,6 +69,7 @@ class HandlePdfController extends ControllerBase { $container->get('fillpdf.context_manager'), $container->get('request_stack'), $container->get('plugin.manager.fillpdf_backend'), + $container->get('plugin.manager.fillpdf_action.processor'), $container->get('token'), $container->get('entity.query'), $container->get('entity.manager') @@ -151,11 +156,10 @@ class HandlePdfController extends ControllerBase { // @todo: When Rules integration ported, emit an event or whatever. - // Determine the appropriate action for the PDF. - - // TODO: figure out what to do about $token_objects. Should I make buildObjects manually re-run everything or just use the final entities passed of each type? Maybe just the latter, since that is what I do in - return $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, []); + $action_response = $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, $context, []); + + return $action_response; } /** @@ -169,82 +173,70 @@ class HandlePdfController extends ControllerBase { * @param string $pdf_data * A string containing the content of the merged PDF. * + * @param array $context + * The FillPDF request context as parsed by + * \Drupal\fillpdf\Service\LinkManipulator. + * * @param array $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 string $action - * One of the following keywords: default, download, save, + * @return NULL|\Symfony\Component\HttpFoundation\Response + * @internal param string $action_plugin_id The default action plugins are: default, download, save,* The default action plugins are: 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 + * 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. - * @todo ^ Make this a plugin too * - * @param array $options - * If set, this function will always end the request by + * You can use any action plugin that implements + * \Drupal\fillpdf\Plugin\FillPdfActionPluginInterface. + * @internal param array|bool $force_download If set, this function will always end the request by* If set, this function will always end the request by * sending the filled PDF to the user's browser. - * - * @return NULL|Response */ - protected function handlePopulatedPdf(FillPdfFormInterface $fillpdf_form, $pdf_data, array $token_objects, $action, array $options = []) { - // TODO: Convert rest of this function. + protected function handlePopulatedPdf(FillPdfFormInterface $fillpdf_form, $pdf_data, $context, array $token_objects) { $force_download = FALSE; - if (!empty($option['force_download'])) { + if (!empty($context['force_download'])) { $force_download = TRUE; } - $valid_actions = [ - 'default', - 'download', - 'save', - 'redirect' - ]; - if (!in_array($action, $valid_actions)) { - // Do nothing if the function is called with an invalid action. - // TODO: Add an assertion here? - return NULL; - } - // 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'; - } + // Determine the appropriate action for the PDF. + // @todo: If they checked destination_redirect, make it redirect + $destination_path_set = !empty($fillpdf_form->destination_path->value); + $redirect = !empty($fillpdf_form->destination_redirect->value); + if ($destination_path_set && !$redirect) { + $action_plugin_id = 'save'; } + elseif ($destination_path_set && $redirect) { + $action_plugin_id = 'redirect'; + } + else { + $action_plugin_id = 'download'; + } + + $action_configuration = [ + 'form' => $fillpdf_form, + 'context' => $context, + 'token_objects' => $token_objects, + 'data' => $pdf_data, + 'generated_filename' => $output_name, + ]; - // 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': - // TODO: Port this to use its own function that in itself will return a file, period. Then handle the redirect logic in the controller instead of as part of the save-to-file method. Also base it off the new code from Drupal 7 - fillpdf_save_to_file($fillpdf_form, $pdf_data, $token_objects, $output_name, !$options, $redirect_to_file); - // FillPDF classic! - case 'download': - $disposition = $response->headers->makeDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $output_name - ); - $response->headers->set('Content-Disposition', $disposition); - break; + /** @var FillPdfActionPluginInterface $fillpdf_action */ + $fillpdf_action = $this->actionManager->createInstance($action_plugin_id, $action_configuration); + $response = $fillpdf_action->execute(); + + // If we are forcing a download, then manually get a Response from + // the download action and return that. Side effects of other plugins will + // still happen, obviously. + if ($force_download) { + /** @var FillPdfDownloadAction $download_action */ + $download_action = $this->actionManager + ->createInstance('download', $action_configuration); + $response = $download_action + ->execute(); } return $response; @@ -252,8 +244,8 @@ class HandlePdfController extends ControllerBase { public function buildFilename($original, array $token_objects) { // Replace tokens *before* sanitization - if (!empty($token_objects)) { - $original = token_replace($original, $token_objects); + if (count($token_objects)) { + $original = $this->token->replace($original, $token_objects); } $output_name = str_replace(' ', '_', $original); diff --git a/src/FillPdfBackendPluginInterface.php b/src/FillPdfBackendPluginInterface.php index 735a1a7..3365460 100644 --- a/src/FillPdfBackendPluginInterface.php +++ b/src/FillPdfBackendPluginInterface.php @@ -10,6 +10,8 @@ namespace Drupal\fillpdf; * Defines the required interface for all FillPDF Backend plugins. * * @package Drupal\fillpdf + * + * @todo: Impement PluginInspectionInterface, ConfigurablePluginInterface and update implementations accordingly. */ interface FillPdfBackendPluginInterface { diff --git a/src/Plugin/FillPdfActionPlugin/FillPdfDownloadAction.php b/src/Plugin/FillPdfActionPlugin/FillPdfDownloadAction.php new file mode 100644 index 0000000..8c809e2 --- /dev/null +++ b/src/Plugin/FillPdfActionPlugin/FillPdfDownloadAction.php @@ -0,0 +1,39 @@ +<?php +/** + * @file + * Contains \Drupal\fillpdf\Plugin\FillPdfActionPlugin\FillPdfDownloadAction. + */ + +namespace Drupal\fillpdf\Plugin\FillPdfActionPlugin; + +use Drupal\Core\Annotation\Translation; +use Drupal\fillpdf\Annotation\FillPdfActionPlugin; +use Drupal\fillpdf\Plugin\FillPdfActionPluginBase; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; + +/** + * Class FillPdfDownloadAction + * @package Drupal\fillpdf\Plugin\FillPdfActionPlugin + * + * @FillPdfActionPlugin( + * id = "download", + * label = @Translation("Download PDF") + * ) + */ +class FillPdfDownloadAction extends FillPdfActionPluginBase { + + public function execute() { + $response = new Response($this->configuration['data']); + + // This ensures that the browser serves the file as a download. + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $this->configuration['generated_filename'] + ); + $response->headers->set('Content-Disposition', $disposition); + + return $response; + } + +} diff --git a/src/Plugin/FillPdfActionPluginBase.php b/src/Plugin/FillPdfActionPluginBase.php index d4a58d5..88c8efa 100644 --- a/src/Plugin/FillPdfActionPluginBase.php +++ b/src/Plugin/FillPdfActionPluginBase.php @@ -10,10 +10,28 @@ namespace Drupal\fillpdf\Plugin; use Drupal\Component\Plugin\PluginBase; /** - * Base class for FillPDF action plugin plugins. + * Base class for FillPDF action plugins. */ abstract class FillPdfActionPluginBase extends PluginBase implements FillPdfActionPluginInterface { - // Add common methods and abstract methods for your plugin type here. + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + public function defaultConfiguration() { + return []; + } + + public function calculateDependencies() { + return []; + } + + public function getConfiguration() { + return $this->configuration; + } + + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } } diff --git a/src/Plugin/FillPdfActionPluginInterface.php b/src/Plugin/FillPdfActionPluginInterface.php index ef3c38c..fc5e7d3 100644 --- a/src/Plugin/FillPdfActionPluginInterface.php +++ b/src/Plugin/FillPdfActionPluginInterface.php @@ -7,23 +7,30 @@ namespace Drupal\fillpdf\Plugin; +use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\PluginInspectionInterface; use Symfony\Component\HttpFoundation\Response; /** * Defines an interface for FillPDF action plugins. * - * Action plugins should ultimately return a + * Action plugins must ultimately return a * \Symfony\Component\HttpFoundation\Response. They may provide additional * methods to provide callers with additional plugin-specific metadata. + * + * They may also have side effects, such as saving a file to the file system. + * They must not, however, end the request. */ -interface FillPdfActionPluginInterface extends PluginInspectionInterface { +interface FillPdfActionPluginInterface extends PluginInspectionInterface, ConfigurablePluginInterface { /** * Take action according to the plugin configuration. This will vary for each * action plugin, but it should do something with the PDF (e.g. prepare a * download response, save it to a file, etc.) and return an appropriate - * Response (or subclass thereof) to the caller. + * Response (or subclass thereof, such as RedirectResponse) to the caller. + * + * When you need context info, see if it is passed to you in + * $this->configuration. * * @return Response * @todo Document exceptions thrown if something goes wrong. -- GitLab