From 6ab00393bae11d80209fef5b31615434e99ed1a8 Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Tue, 3 Mar 2020 22:58:44 +0100
Subject: [PATCH] Added pdf export output

---
 CHANGELOG.md                                  |   1 +
 CRM/DataprocessorOutputExport/CSV.php         |   2 +-
 CRM/DataprocessorOutputExport/PDF.php         | 525 ++++++++++++++++++
 Civi/DataProcessor/Factory.php                |   1 +
 .../Form/Configuration/PDF.tpl                |  79 +++
 5 files changed, 607 insertions(+), 1 deletion(-)
 create mode 100644 CRM/DataprocessorOutputExport/PDF.php
 create mode 100644 templates/CRM/DataprocessorOutputExport/Form/Configuration/PDF.tpl

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0db15608..0f022636 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 * Added source to retrieve the owner membership, when owner membership is not set (meaning it is already the primary) then it will return itself.
 * Added date filter to filter date with the PHP Date Format.
 * Added filtering on Contact (sub) type on the contact filter.
+* Added PDF Export Output
 
 # Version 1.3.0
 
diff --git a/CRM/DataprocessorOutputExport/CSV.php b/CRM/DataprocessorOutputExport/CSV.php
index f3ceed55..d03c216c 100644
--- a/CRM/DataprocessorOutputExport/CSV.php
+++ b/CRM/DataprocessorOutputExport/CSV.php
@@ -148,7 +148,7 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface, Direct
    * @throws \Exception
    */
   public function downloadExport(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
-    if (!$dataProcessorClass->getDataFlow()->recordCount() > self::MAX_DIRECT_SIZE) {
+    if ($dataProcessorClass->getDataFlow()->recordCount() > self::MAX_DIRECT_SIZE) {
       $this->startBatchJob($dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName, $sortDirection, $idField, $selectedIds);
     } else {
       $this->doDirectDownload($dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName, $sortDirection, $idField, $selectedIds);
diff --git a/CRM/DataprocessorOutputExport/PDF.php b/CRM/DataprocessorOutputExport/PDF.php
new file mode 100644
index 00000000..f2122c75
--- /dev/null
+++ b/CRM/DataprocessorOutputExport/PDF.php
@@ -0,0 +1,525 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use Civi\DataProcessor\Output\ExportOutputInterface;
+use Civi\DataProcessor\Output\DirectDownloadExportOutputInterface;
+
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+class CRM_DataprocessorOutputExport_PDF implements ExportOutputInterface, DirectDownloadExportOutputInterface {
+
+  const MAX_DIRECT_SIZE = 500;
+
+  const RECORDS_PER_JOB = 250;
+
+  /**
+   * Returns true when this filter has additional configuration
+   *
+   * @return bool
+   */
+  public function hasConfiguration() {
+    return true;
+  }
+
+  /**
+   * When this filter type has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $filter
+   */
+  public function buildConfigurationForm(\CRM_Core_Form $form, $output=array()) {
+    $dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $output['data_processor_id']));
+    $dataProcessorClass = \CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($dataProcessor);
+    $fields = array();
+    foreach($dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
+      $field = $outputFieldHandler->getOutputFieldSpecification();
+      $fields[$field->alias] = $field->title;
+    }
+
+    $pdfFormats = array();
+    $pdfFormatsApi = civicrm_api3('OptionValue', 'get', ['option_group_id' => 'pdf_format', 'options' => ['limit' => 0]]);
+    foreach($pdfFormatsApi['values'] as $pdfFormat) {
+      $pdfFormats[$pdfFormat['id']] = $pdfFormat['label'];
+    }
+
+    $form->add('select', 'hidden_fields', E::ts('Hidden fields'), $fields, false, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'multiple' => true,
+      'placeholder' => E::ts('- select -'),
+    ));
+
+    $form->add('select', 'pdf_format', E::ts('PDF Format'), $pdfFormats, false, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'multiple' => false,
+      'placeholder' => E::ts('- Default PDF Format -'),
+    ));
+    $form->assign('ManagePdfFormatUrl', CRM_Utils_System::url('civicrm/admin/pdfFormats', ['reset'=>1]));
+
+    $borderStyles = [
+      'full' => E::ts('Border around every cell'),
+      'row_bottom' => E::ts('Border at the bottom of the row'),
+    ];
+    $form->add('select', 'border', E::ts('Border'), $borderStyles, false, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'multiple' => false,
+      'placeholder' => E::ts('- No border -'),
+    ));
+
+    $form->add('checkbox', 'anonymous', E::ts('Available for anonymous users'), array(), false);
+
+    $form->add('wysiwyg', 'header', E::ts('Header'), array('rows' => 6, 'cols' => 80));
+
+    $form->add('checkbox', 'additional_column', E::ts('Add an additional column'), array(), false);
+    $form->add('text', 'additional_column_title', E::ts('Additional column title'));
+    $form->add('text', 'additional_column_width', E::ts('Additional column width'));
+    $form->add('text', 'additional_column_height', E::ts('Additional column height'));
+
+    $configuration = false;
+    if ($output && isset($output['configuration'])) {
+      $configuration = $output['configuration'];
+    }
+    if ($configuration && isset($configuration['hidden_fields'])) {
+      $defaults['hidden_fields'] = $configuration['hidden_fields'];
+    }
+    if ($configuration && isset($configuration['pdf_format'])) {
+      $defaults['pdf_format'] = $configuration['pdf_format'];
+    }
+    if ($configuration && isset($configuration['header'])) {
+      $defaults['header'] = $configuration['header'];
+    }
+    if ($configuration && isset($configuration['anonymous'])) {
+      $defaults['anonymous'] = $configuration['anonymous'];
+    }
+    if ($configuration && isset($configuration['additional_column'])) {
+      $defaults['additional_column'] = $configuration['additional_column'];
+    }
+    if ($configuration && isset($configuration['additional_column_title'])) {
+      $defaults['additional_column_title'] = $configuration['additional_column_title'];
+    }
+    if ($configuration && isset($configuration['additional_column_width'])) {
+      $defaults['additional_column_width'] = $configuration['additional_column_width'];
+    }
+    if ($configuration && isset($configuration['additional_column_height'])) {
+      $defaults['additional_column_height'] = $configuration['additional_column_height'];
+    }
+    if ($configuration && isset($configuration['border'])) {
+      $defaults['border'] = $configuration['border'];
+    }
+    $form->setDefaults($defaults);
+  }
+
+  /**
+   * When this filter type has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName() {
+    return "CRM/DataprocessorOutputExport/Form/Configuration/PDF.tpl";
+  }
+
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @param array $output
+   * @return array
+   */
+  public function processConfiguration($submittedValues, &$output) {
+    $configuration = array();
+    $configuration['hidden_fields'] = $submittedValues['hidden_fields'];
+    $configuration['pdf_format'] = $submittedValues['pdf_format'];
+    $configuration['header'] = $submittedValues['header'];
+    $configuration['anonymous'] = $submittedValues['anonymous'];
+    $configuration['additional_column'] = $submittedValues['additional_column'];
+    $configuration['additional_column_title'] = $submittedValues['additional_column_title'];
+    $configuration['additional_column_width'] = $submittedValues['additional_column_width'];
+    $configuration['additional_column_height'] = $submittedValues['additional_column_height'];
+    $configuration['border'] = $submittedValues['border'];
+    return $configuration;
+  }
+
+  /**
+   * This function is called prior to removing an output
+   *
+   * @param array $output
+   * @return void
+   */
+  public function deleteOutput($output) {
+    // Do nothing
+  }
+
+
+  /**
+   * Returns the mime type of the export file.
+   *
+   * @return string
+   */
+  public function mimeType() {
+    return 'application/pdf';
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getTitleForExport($output, $dataProcessor) {
+    return E::ts('Download as PDF');
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string|false
+   */
+  public function getExportFileIcon($output, $dataProcessor) {
+    return '<i class="fa fa-file-pdf-o">&nbsp;</i>';
+  }
+
+  /**
+   * Download export
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass
+   * @param array $dataProcessor
+   * @param array $outputBAO
+   * @param array $formValues
+   * @param string $sortFieldName
+   * @param string $sortDirection
+   * @param string $idField
+   *  Set $idField to the name of the field containing the ID of the array $selectedIds
+   * @param array $selectedIds
+   *   Array with the selectedIds.
+   * @return string
+   * @throws \Exception
+   */
+  public function downloadExport(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
+    if ($dataProcessorClass->getDataFlow()->recordCount() > self::MAX_DIRECT_SIZE) {
+      $this->startBatchJob($dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName, $sortDirection, $idField, $selectedIds);
+    } else {
+      $this->doDirectDownload($dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName, $sortDirection, $idField, $selectedIds);
+    }
+  }
+
+  public function doDirectDownload(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName = null, $sortDirection = 'ASC', $idField, $selectedIds=array()) {
+    $filename = date('Ymdhis').'_'.$dataProcessor['id'].'_'.$outputBAO['id'].'_'.CRM_Core_Session::getLoggedInContactID().'_'.$dataProcessor['name'].'.html';
+    $download_name = date('Ymdhis').'_'.$dataProcessor['name'].'.pdf';
+
+    $basePath = CRM_Core_Config::singleton()->templateCompileDir . 'dataprocessor_export_pdf';
+    CRM_Utils_File::createDir($basePath);
+    CRM_Utils_File::restrictAccess($basePath.'/');
+
+    $path = CRM_Core_Config::singleton()->templateCompileDir . 'dataprocessor_export_pdf/'. $filename;
+    if ($sortFieldName) {
+      $dataProcessorClass->getDataFlow()->addSort($sortFieldName, $sortDirection);
+    }
+
+    self::createHeader($path, $dataProcessorClass, $outputBAO['configuration'], $dataProcessor);
+    self::exportDataProcessor($path, $dataProcessorClass, $outputBAO['configuration'], $idField, $selectedIds);
+    $path = self::createFooter($path, $dataProcessorClass, $outputBAO['configuration'], $dataProcessor);
+
+    $mimeType = $this->mimeType();
+
+    if (!$path) {
+      \CRM_Core_Error::statusBounce('Could not retrieve the file');
+    }
+
+    $buffer = file_get_contents($path);
+    if (!$buffer) {
+      \CRM_Core_Error::statusBounce('The file is either empty or you do not have permission to retrieve the file');
+    }
+
+    \CRM_Utils_System::download(
+      $download_name,
+      $mimeType,
+      $buffer,
+      NULL,
+      TRUE,
+      'download'
+    );
+  }
+
+
+  protected function startBatchJob(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
+    $session = CRM_Core_Session::singleton();
+
+    $name = date('Ymdhis').'_'.$dataProcessor['id'].'_'.$outputBAO['id'].'_'.CRM_Core_Session::getLoggedInContactID().'_'.$dataProcessor['name'];
+
+    $queue = CRM_Queue_Service::singleton()->create(array(
+      'type' => 'Sql',
+      'name' => $name,
+      'reset' => TRUE, //do flush queue upon creation
+    ));
+
+    $basePath = CRM_Core_Config::singleton()->templateCompileDir . 'dataprocessor_export_pdf';
+    CRM_Utils_File::createDir($basePath);
+    CRM_Utils_File::restrictAccess($basePath.'/');
+    $filename = $basePath.'/'. $name.'.html';
+
+    self::createHeader($filename, $dataProcessorClass, $outputBAO['configuration'], $dataProcessor);
+
+    $count = $dataProcessorClass->getDataFlow()->recordCount();
+    $recordsPerJob = self::RECORDS_PER_JOB;
+    for($i=0; $i < $count; $i = $i + $recordsPerJob) {
+      $title = E::ts('Exporting records %1/%2', array(
+        1 => ($i+$recordsPerJob) <= $count ? $i+$recordsPerJob : $count,
+        2 => $count,
+      ));
+
+      //create a task without parameters
+      $task = new CRM_Queue_Task(
+        array(
+          'CRM_DataprocessorOutputExport_PDF',
+          'exportBatch'
+        ), //call back method
+        array($filename,$formValues, $dataProcessor['id'], $outputBAO['id'], $i, $recordsPerJob, $sortFieldName, $sortDirection, $idField, $selectedIds), //parameters,
+        $title
+      );
+      //now add this task to the queue
+      $queue->createItem($task);
+    }
+
+    $task = new CRM_Queue_Task(
+      array(
+        'CRM_DataprocessorOutputExport_PDF',
+        'exportBatchFooter'
+      ), //call back method
+      array($filename,$formValues, $dataProcessor['id'], $outputBAO['id'], $i, $recordsPerJob, $sortFieldName, $sortDirection, $idField, $selectedIds), //parameters,
+      $title
+    );
+    //now add this task to the queue
+    $queue->createItem($task);
+
+    $url = str_replace("&amp;", "&", $session->readUserContext());
+
+    $runner = new CRM_Queue_Runner(array(
+      'title' => E::ts('Exporting data'), //title fo the queue
+      'queue' => $queue, //the queue object
+      'errorMode'=> CRM_Queue_Runner::ERROR_CONTINUE, //abort upon error and keep task in queue
+      'onEnd' => array('CRM_DataprocessorOutputExport_PDF', 'onEnd'), //method which is called as soon as the queue is finished
+      'onEndUrl' => $url,
+    ));
+
+    $runner->runAllViaWeb(); // does not return
+  }
+
+  protected static function createHeader($filename, \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $configuration, $dataProcessor) {
+    $borderStyle = "";
+    if (isset($configuration['border']) && $configuration['border'] == 'full') {
+      $borderStyle = "th, td { border: 1px solid black; padding-left: 4px; padding-right: 4px; }";
+    } elseif (isset($configuration['border']) && $configuration['border'] == 'row_bottom') {
+      $borderStyle = "th, td { border-bottom: 1px solid black; }";
+    }
+
+    $content = "
+        <html>
+        <head>
+        <title>{$dataProcessor['title']}</title>
+        </head>
+        <body>
+        <style>
+        table { border-collapse: collapse; }
+        {$borderStyle}
+        </style>
+        <h1>{$dataProcessor['title']}</h1>
+    ";
+
+    if (isset($configuration['header'])) {
+      $content .= $configuration['header'];
+    }
+
+    $content .= "<table style=\"width: 100%;\"><thead><tr>";
+    $hiddenFields = array();
+    if (isset($configuration['hidden_fields']) && is_array($configuration['hidden_fields'])) {
+      $hiddenFields = $configuration['hidden_fields'];
+    }
+    foreach($dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputHandler) {
+      if (!in_array($outputHandler->getOutputFieldSpecification()->alias, $hiddenFields)) {
+        $content .= "<th>" . self::encodeValue($outputHandler->getOutputFieldSpecification()->title) . "</th>";
+      }
+    }
+
+    if (isset($configuration['additional_column']) && $configuration['additional_column']) {
+      $additionalColumnStyle = "";
+      if (isset($configuration['additional_column_width']) && $configuration['additional_column_width']) {
+        $additionalColumnStyle .= "width: {$configuration['additional_column_width']};";
+      }
+      $content .= "<th style=\"{$additionalColumnStyle}\">";
+      if (isset($configuration['additional_column_title']) && $configuration['additional_column_title']) {
+        $content .= $configuration['additional_column_title'];
+      }
+      $content .= "</th>";
+    }
+
+    $content .= "</tr></thead>";
+    $file = fopen($filename, 'a');
+    fwrite($file, $content."\r\n");
+    fclose($file);
+  }
+
+  protected static function createFooter($filename, \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $configuration, $dataProcessor) {
+    $content = "</table></body></html>";
+    $file = fopen($filename, 'a');
+    fwrite($file, $content."\r\n");
+    fclose($file);
+
+    $content = file_get_contents($filename);
+    unlink($filename);
+    $pdfFilename = str_replace(".html", ".pdf", $filename);
+    $pdfFormat = isset($configuration['pdf_format']) ? $configuration['pdf_format'] : null;
+    $pdfContents = \CRM_Utils_PDF_Utils::html2pdf($content, basename($pdfFilename), TRUE, $pdfFormat);
+    $file = fopen($pdfFilename, 'a');
+    fwrite($file, $pdfContents."\r\n");
+    fclose($file);
+
+    return $pdfFilename;
+  }
+
+  protected static function exportDataProcessor($filename, \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessor, $configuration, $idField, $selectedIds=array()) {
+    $hiddenFields = array();
+    if (isset($configuration['hidden_fields']) && is_array($configuration['hidden_fields'])) {
+      $hiddenFields = $configuration['hidden_fields'];
+    }
+
+    $additionalColumn = '';
+    if (isset($configuration['additional_column']) && $configuration['additional_column']) {
+      $style = "";
+      if (isset($configuration['additional_column_width']) && $configuration['additional_column_width']) {
+        $style .= "width: {$configuration['additional_column_width']};";
+      }
+      if (isset($configuration['additional_column_height']) && $configuration['additional_column_height']) {
+        $style .= "height: {$configuration['additional_column_height']};";
+      }
+      $additionalColumn = "<td style=\"{$style}\">&nbsp;</td>";
+    }
+
+    $content = "";
+    try {
+      while($record = $dataProcessor->getDataFlow()->nextRecord()) {
+        $row = array();
+        $rowIsSelected = true;
+        if (isset($idField) && is_array($selectedIds) && count($selectedIds)) {
+          $rowIsSelected = false;
+          $id = $record[$idField]->rawValue;
+          if (in_array($id, $selectedIds)) {
+            $rowIsSelected = true;
+          }
+        }
+        if ($rowIsSelected) {
+          $content .= "<tr>";
+          foreach ($record as $field => $value) {
+            if (!in_array($field, $hiddenFields)) {
+              $content .= "<td>" . self::encodeValue($value->formattedValue) . "</td>";
+            }
+          }
+          $content .= $additionalColumn . "</tr>";
+        }
+      }
+    } catch (\Civi\DataProcessor\DataFlow\EndOfFlowException $e) {
+      // Do nothing
+    }
+    $file = fopen($filename, 'a');
+    fwrite($file, $content . "\r\n");
+    fclose($file);
+  }
+
+  protected static function encodeValue($value) {
+    return htmlentities($value);
+  }
+
+  public static function exportBatch(CRM_Queue_TaskContext $ctx, $filename, $params, $dataProcessorId, $outputId, $offset, $limit, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
+    $dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $dataProcessorId));
+    $output = civicrm_api3('DataProcessorOutput', 'getsingle', array('id' => $outputId));
+    $dataProcessorClass = \CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($dataProcessor);
+    CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::applyFilters($dataProcessorClass, $params);
+    if ($sortFieldName) {
+      $dataProcessorClass->getDataFlow()->addSort($sortFieldName, $sortDirection);
+    }
+    $dataProcessorClass->getDataFlow()->setOffset($offset);
+    $dataProcessorClass->getDataFlow()->setLimit($limit);
+    self::exportDataProcessor($filename, $dataProcessorClass, $output['configuration'], $idField, $selectedIds);
+    return TRUE;
+  }
+
+  public static function exportBatchFooter(CRM_Queue_TaskContext $ctx, $filename, $params, $dataProcessorId, $outputId, $offset, $limit, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
+    $dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $dataProcessorId));
+    $output = civicrm_api3('DataProcessorOutput', 'getsingle', array('id' => $outputId));
+    $dataProcessorClass = \CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($dataProcessor);
+    CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::applyFilters($dataProcessorClass, $params);
+    if ($sortFieldName) {
+      $dataProcessorClass->getDataFlow()->addSort($sortFieldName, $sortDirection);
+    }
+    $dataProcessorClass->getDataFlow()->setOffset($offset);
+    $dataProcessorClass->getDataFlow()->setLimit($limit);
+    self::createFooter($filename, $dataProcessorClass, $output['configuration'], $dataProcessor);
+    return TRUE;
+  }
+
+  public static function onEnd(CRM_Queue_TaskContext $ctx) {
+    $queue_name = $ctx->queue->getName();
+    $pdf_filename = $queue_name.'.pdf';
+    $downloadLink = CRM_Utils_System::url('civicrm/dataprocessor/form/output/download', 'filename='.$pdf_filename.'&directory=dataprocessor_export_pdf');
+    //set a status message for the user
+    CRM_Core_Session::setStatus(E::ts('<a href="%1">Download PDF file</a>', array(1=>$downloadLink)), E::ts('Exported data'), 'success');
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getUrl($output, $dataProcessor) {
+    return CRM_Utils_System::url('civicrm/dataprocessor/output/export', array(
+      'name' => $dataProcessor['name'],
+      'type' => $output['type']
+    ));
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getTitleForLink($output, $dataProcessor) {
+    return $dataProcessor['title'];
+  }
+
+  /**
+   * Checks whether the current user has access to this output
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return bool
+   */
+  public function checkPermission($output, $dataProcessor) {
+    $anonymous = false;
+    if (isset($output['configuration']) && isset($output['configuration']['anonymous'])) {
+      $anonymous = $output['configuration']['anonymous'] ? true : false;
+    }
+    $userId = \CRM_Core_Session::getLoggedInContactID();
+    if ($userId) {
+      return true;
+    } elseif ($anonymous) {
+      return true;
+    }
+    return false;
+  }
+
+
+}
diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php
index 2f9aef93..74826f10 100644
--- a/Civi/DataProcessor/Factory.php
+++ b/Civi/DataProcessor/Factory.php
@@ -133,6 +133,7 @@ class Factory {
     $this->addOutput('membership_search', new Definition('CRM_DataprocessorSearch_MembershipSearch'), E::ts('Membership Search'));
     $this->addOutput('participant_search', new Definition('CRM_DataprocessorSearch_ParticipantSearch'), E::ts('Participant Search'));
     $this->addOutput('export_csv', new Definition('CRM_DataprocessorOutputExport_CSV'), E::ts('CSV Export'));
+    $this->addOutput('export_pdf', new Definition('CRM_DataprocessorOutputExport_PDF'), E::ts('PDF Export'));
     $this->addFilter('simple_sql_filter', new Definition('Civi\DataProcessor\FilterHandler\SimpleSqlFilter'), E::ts('Field filter'));
     $this->addFilter('date_filter', new Definition('Civi\DataProcessor\FilterHandler\DateFilter'), E::ts('Date filter'));
     $this->addFilter('multiple_field_filter', new Definition('Civi\DataProcessor\FilterHandler\MultipleFieldFilter'), E::ts('Text in multiple fields Filter'));
diff --git a/templates/CRM/DataprocessorOutputExport/Form/Configuration/PDF.tpl b/templates/CRM/DataprocessorOutputExport/Form/Configuration/PDF.tpl
new file mode 100644
index 00000000..a158ec29
--- /dev/null
+++ b/templates/CRM/DataprocessorOutputExport/Form/Configuration/PDF.tpl
@@ -0,0 +1,79 @@
+{crmScope extensionKey='dataprocessor'}
+  <div class="crm-section">
+    <div class="label">{$form.pdf_format.label}</div>
+    <div class="content">{$form.pdf_format.html}
+      <p class="description">
+        {ts 1=$ManagePdfFormatUrl}You can manage PDF Formats at <a href="%1">Administer --> Communications --> PDF Formats</a>{/ts}
+      </p>
+    </div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.border.label}</div>
+    <div class="content">{$form.border.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.header.label}</div>
+    <div class="content">{$form.header.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.hidden_fields.label}</div>
+    <div class="content">{$form.hidden_fields.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.additional_column.label}</div>
+    <div class="content">{$form.additional_column.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section additional_column">
+    <div class="label">{$form.additional_column_title.label}</div>
+    <div class="content">{$form.additional_column_title.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section additional_column">
+    <div class="label">{$form.additional_column_width.label}</div>
+    <div class="content">{$form.additional_column_width.html}
+      <p class="description">{ts}E.g. 2 cm{/ts}</p>
+    </div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section additional_column">
+    <div class="label">{$form.additional_column_height.label}</div>
+    <div class="content">{$form.additional_column_height.html}
+      <p class="description">{ts}E.g. 2 cm{/ts}</p>
+    </div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.anonymous.label}</div>
+    <div class="content">{$form.anonymous.html}
+      <p class="description">
+        {ts}Tick this box when you want to make the PDF available for non-logged in users. <br>
+        <strong>Caution:</strong> when you check this box the data becomes available without logging so this might lead to a data breach.{/ts}</p>
+    </div>
+    <div class="clear"></div>
+  </div>
+
+  <script type="text/javascript">
+    {literal}
+    CRM.$(function($) {
+      function toggleAdditionalColumn() {
+        if ($('#additional_column').prop('checked')) {
+          $('.crm-section.additional_column').show();
+        } else {
+          $('.crm-section.additional_column').hide();
+        }
+      }
+
+      $('#additional_column').on('click', toggleAdditionalColumn);
+      $('#additional_column').on('keypress', toggleAdditionalColumn);
+
+      //$('#additional_column').trigger('change');
+      toggleAdditionalColumn();
+    });
+    {/literal}
+  </script>
+{/crmScope}
-- 
GitLab