From 5486d24ed555e38653b420d148ab981a310ed231 Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Tue, 28 Jan 2020 20:52:11 +0100
Subject: [PATCH] Made CSV Export download available for anonymous users.

---
 CHANGELOG.md                                  |   4 +
 CRM/Dataprocessor/Form/DataProcessor.php      |   2 +
 .../Form/ManageDataProcessors.php             |   6 +-
 CRM/DataprocessorOutputExport/CSV.php         |  59 +++++++-
 CRM/DataprocessorOutputExport/Page/Export.php | 136 ++++++++++++++++++
 .../DirectDownloadExportOutputInterface.php   |  33 +++++
 .../Output/UrlOutputInterface.php             |  43 ++++++
 .../Form/Configuration/CSV.tpl                |  13 +-
 xml/Menu/dataprocessor.xml                    |   9 +-
 9 files changed, 299 insertions(+), 6 deletions(-)
 create mode 100644 CRM/DataprocessorOutputExport/Page/Export.php
 create mode 100644 Civi/DataProcessor/Output/DirectDownloadExportOutputInterface.php
 create mode 100644 Civi/DataProcessor/Output/UrlOutputInterface.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ba16bba..8f68df0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# Version 1.2.0 (not yet released)
+
+* Made CSV Export download available for anonymous users.
+
 # Version 1.1.0
 
 * Respect selected permissions for outputs
diff --git a/CRM/Dataprocessor/Form/DataProcessor.php b/CRM/Dataprocessor/Form/DataProcessor.php
index edd4f7ad..32504fac 100644
--- a/CRM/Dataprocessor/Form/DataProcessor.php
+++ b/CRM/Dataprocessor/Form/DataProcessor.php
@@ -125,6 +125,8 @@ class CRM_Dataprocessor_Form_DataProcessor extends CRM_Core_Form {
       $outputClass = $factory->getOutputByName($output['type']);
       if ($outputClass instanceof \Civi\DataProcessor\Output\UIOutputInterface) {
         $outputs[$idx]['navigation_url'] = CRM_Utils_System::url($outputClass->getUrlToUi($output, $this->dataProcessor), array('reset' => '1'));
+      } elseif ($outputClass instanceof \Civi\DataProcessor\Output\UrlOutputInterface && $outputClass->checkPermission($output, $this->dataProcessor)) {
+        $outputs[$idx]['navigation_url'] = $outputClass->getUrl($output, $this->dataProcessor);
       }
 
       if (isset($types[$output['type']])) {
diff --git a/CRM/Dataprocessor/Form/ManageDataProcessors.php b/CRM/Dataprocessor/Form/ManageDataProcessors.php
index 085cff92..67dfca7a 100644
--- a/CRM/Dataprocessor/Form/ManageDataProcessors.php
+++ b/CRM/Dataprocessor/Form/ManageDataProcessors.php
@@ -41,7 +41,11 @@ class CRM_Dataprocessor_Form_ManageDataProcessors extends CRM_Core_Form {
         if ($outputClass instanceof \Civi\DataProcessor\Output\UIOutputInterface) {
           $dataProcessors[$idx]['navigation'][$outputIndex]['url'] = CRM_Utils_System::url($outputClass->getUrlToUi($output, $dataProcessor), array('reset' => '1'));
           $dataProcessors[$idx]['navigation'][$outputIndex]['title'] = $outputClass->getTitleForUiLink($output, $dataProcessor);
+        } elseif ($outputClass instanceof \Civi\DataProcessor\Output\UrlOutputInterface && $outputClass->checkPermission($output, $dataProcessor)) {
+          $dataProcessors[$idx]['navigation'][$outputIndex]['url'] = $outputClass->getUrl($output, $dataProcessor);
+          $dataProcessors[$idx]['navigation'][$outputIndex]['title'] = $outputClass->getTitleForLink($output, $dataProcessor);
         }
+
       }
 
     }
@@ -78,4 +82,4 @@ class CRM_Dataprocessor_Form_ManageDataProcessors extends CRM_Core_Form {
 
   }
 
-}
\ No newline at end of file
+}
diff --git a/CRM/DataprocessorOutputExport/CSV.php b/CRM/DataprocessorOutputExport/CSV.php
index 62c5352c..f3ceed55 100644
--- a/CRM/DataprocessorOutputExport/CSV.php
+++ b/CRM/DataprocessorOutputExport/CSV.php
@@ -5,10 +5,11 @@
  */
 
 use Civi\DataProcessor\Output\ExportOutputInterface;
+use Civi\DataProcessor\Output\DirectDownloadExportOutputInterface;
 
 use CRM_Dataprocessor_ExtensionUtil as E;
 
-class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
+class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface, DirectDownloadExportOutputInterface {
 
   const MAX_DIRECT_SIZE = 500;
 
@@ -34,6 +35,7 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
     $form->add('text', 'delimiter', E::ts('Delimiter'), array(), true);
     $form->add('text', 'enclosure', E::ts('Enclosure'), array(), true);
     $form->add('text', 'escape_char', E::ts('Escape char'), array(), true);
+    $form->add('checkbox', 'anonymous', E::ts('Available for anonymous users'), array(), false);
 
     $configuration = false;
     if ($output && isset($output['configuration'])) {
@@ -54,6 +56,9 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
     } else {
       $defaults['escape_char'] = '\\';
     }
+    if ($configuration && isset($configuration['anonymous'])) {
+      $defaults['anonymous'] = $configuration['anonymous'];
+    }
     $form->setDefaults($defaults);
   }
 
@@ -80,6 +85,7 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
     $configuration['delimiter'] = $submittedValues['delimiter'];
     $configuration['enclosure'] = $submittedValues['enclosure'];
     $configuration['escape_char'] = $submittedValues['escape_char'];
+    $configuration['anonymous'] = $submittedValues['anonymous'];
     return $configuration;
   }
 
@@ -139,16 +145,17 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
    * @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) {
+    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);
     }
   }
 
-  protected function doDirectDownload(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName = null, $sortDirection = 'ASC', $idField, $selectedIds=array()) {
+  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'].'.csv';
     $download_name = date('Ymdhis').'_'.$dataProcessor['name'].'.csv';
 
@@ -306,5 +313,51 @@ class CRM_DataprocessorOutputExport_CSV implements ExportOutputInterface {
     CRM_Core_Session::setStatus(E::ts('<a href="%1">Download CSV 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/CRM/DataprocessorOutputExport/Page/Export.php b/CRM/DataprocessorOutputExport/Page/Export.php
new file mode 100644
index 00000000..be1c5dd2
--- /dev/null
+++ b/CRM/DataprocessorOutputExport/Page/Export.php
@@ -0,0 +1,136 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+class CRM_DataprocessorOutputExport_Page_Export extends CRM_Core_Page {
+
+  /**
+   * @var array
+   */
+  protected $dataProcessor;
+
+  /**
+   * @var \Civi\DataProcessor\ProcessorType\AbstractProcessorType;
+   */
+  protected $dataProcessorClass;
+
+  /**
+   * @var int
+   */
+  protected $dataProcessorId;
+
+  /**
+   * @var \CRM_Dataprocessor_BAO_Output
+   */
+  protected $dataProcessorOutput;
+
+  /**
+   * Run page.
+   */
+  public function run() {
+    $this->loadDataProcessor();
+    $sortFields = $this->addColumnHeaders();
+    $this->sort = new CRM_Utils_Sort($sortFields);
+
+    $this->runExport();
+  }
+
+  protected function runExport() {
+    $factory = dataprocessor_get_factory();
+    CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::applyFilters($this->dataProcessorClass, array());
+
+    // Set the sort
+    $sortDirection = 'ASC';
+    $sortFieldName = null;
+    if (!empty($this->sort->_vars[$this->sort->getCurrentSortID()])) {
+      $sortField = $this->sort->_vars[$this->sort->getCurrentSortID()];
+      if ($this->sort->getCurrentSortDirection() == CRM_Utils_Sort::DESCENDING) {
+        $sortDirection = 'DESC';
+      }
+      $sortFieldName = $sortField['name'];
+    }
+
+    $outputClass = $factory->getOutputByName($this->dataProcessorOutput['type']);
+    if ($outputClass instanceof \Civi\DataProcessor\Output\DirectDownloadExportOutputInterface) {
+      $outputClass->doDirectDownload($this->dataProcessorClass, $this->dataProcessor, $this->dataProcessorOutput, array(), $sortFieldName, $sortDirection);
+    }
+    throw new \Exception('Unable to export');
+  }
+
+  protected function getDataProcessorName() {
+    return CRM_Utils_Request::retrieve('name', 'String', CRM_Core_DAO::$_nullObject, TRUE);
+  }
+
+  protected function getOutputName() {
+    return CRM_Utils_Request::retrieve('type', 'String', CRM_Core_DAO::$_nullObject, TRUE);
+  }
+
+  /**
+   * Retrieve the data processor and the output configuration
+   *
+   * @throws \Exception
+   */
+  protected function loadDataProcessor() {
+    $factory = dataprocessor_get_factory();
+    if (!$this->dataProcessorId) {
+      $dataProcessorName = $this->getDataProcessorName();
+      $sql = "
+        SELECT civicrm_data_processor.id as data_processor_id,  civicrm_data_processor_output.id AS output_id
+        FROM civicrm_data_processor
+        INNER JOIN civicrm_data_processor_output ON civicrm_data_processor.id = civicrm_data_processor_output.data_processor_id
+        WHERE is_active = 1 AND civicrm_data_processor.name = %1 AND civicrm_data_processor_output.type = %2
+      ";
+      $params[1] = [$dataProcessorName, 'String'];
+      $params[2] = [$this->getOutputName(), 'String'];
+      $dao = CRM_Dataprocessor_BAO_DataProcessor::executeQuery($sql, $params, TRUE, 'CRM_Dataprocessor_BAO_DataProcessor');
+      if (!$dao->fetch()) {
+        throw new \Exception('Could not find Data Processor "' . $dataProcessorName.'"');
+      }
+
+      $this->dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $dao->data_processor_id));
+      $this->dataProcessorClass = \CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($this->dataProcessor, true);
+      $this->dataProcessorId = $dao->data_processor_id;
+
+      $this->dataProcessorOutput = civicrm_api3('DataProcessorOutput', 'getsingle', array('id' => $dao->output_id));
+
+      $outputClass = $factory->getOutputByName($this->dataProcessorOutput['type']);
+      if (!$outputClass instanceof \Civi\DataProcessor\Output\DirectDownloadExportOutputInterface) {
+        throw new \Exception('Invalid output');
+      }
+      if (!$outputClass->checkPermission($this->dataProcessorOutput, $this->dataProcessor)) {
+        CRM_Utils_System::permissionDenied();
+        CRM_Utils_System::civiExit();
+      }
+    }
+  }
+
+  /**
+   * Add the headers for the columns
+   *
+   * @return array
+   *   Array with all possible sort fields.
+   *
+   * @throws \Civi\DataProcessor\DataFlow\InvalidFlowException
+   */
+  protected function addColumnHeaders() {
+    $sortFields = array();
+    $columnHeaders = array();
+    $sortColumnNr = 1;
+    foreach($this->dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
+      $field = $outputFieldHandler->getOutputFieldSpecification();
+      $columnHeaders[$field->alias] = $field->title;
+      if ($outputFieldHandler instanceof \Civi\DataProcessor\FieldOutputHandler\OutputHandlerSortable) {
+        $sortFields[$sortColumnNr] = array(
+          'name' => $field->title,
+          'sort' => $field->alias,
+          'direction' => CRM_Utils_Sort::DONTCARE,
+        );
+        $sortColumnNr++;
+      }
+    }
+    return $sortFields;
+  }
+
+}
diff --git a/Civi/DataProcessor/Output/DirectDownloadExportOutputInterface.php b/Civi/DataProcessor/Output/DirectDownloadExportOutputInterface.php
new file mode 100644
index 00000000..0a5313b4
--- /dev/null
+++ b/Civi/DataProcessor/Output/DirectDownloadExportOutputInterface.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+namespace Civi\DataProcessor\Output;
+
+/**
+ * This interface indicates that the output type is available as a direct download.
+ *
+ * @package Civi\DataProcessor\Output
+ */
+interface DirectDownloadExportOutputInterface extends UrlOutputInterface {
+
+  /**
+   * 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
+   */
+  public function doDirectDownload(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName = null, $sortDirection = 'ASC', $idField, $selectedIds=array());
+
+}
diff --git a/Civi/DataProcessor/Output/UrlOutputInterface.php b/Civi/DataProcessor/Output/UrlOutputInterface.php
new file mode 100644
index 00000000..fd14a754
--- /dev/null
+++ b/Civi/DataProcessor/Output/UrlOutputInterface.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+namespace Civi\DataProcessor\Output;
+
+/**
+ * This interface indicates that the output type is available through a url.
+ *
+ * @package Civi\DataProcessor\Output
+ */
+interface UrlOutputInterface {
+
+  /**
+   * 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);
+
+  /**
+   * 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);
+
+  /**
+   * Checks whether the current user has access to this output
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return bool
+   */
+  public function checkPermission($output, $dataProcessor);
+
+}
diff --git a/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl b/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl
index d6e41a9c..e23712f3 100644
--- a/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl
+++ b/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl
@@ -15,5 +15,16 @@
         <div class="content">{$form.escape_char.html}</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 CSV available for non-logged in users. <br>
+        This could be necessary when another system is importing this csv file on a regular basis. E.g. a website with
+        a public agenda of the upcoming events.
+        <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>
 
-{/crmScope}
\ No newline at end of file
+{/crmScope}
diff --git a/xml/Menu/dataprocessor.xml b/xml/Menu/dataprocessor.xml
index 7ef77365..c237e1ac 100644
--- a/xml/Menu/dataprocessor.xml
+++ b/xml/Menu/dataprocessor.xml
@@ -83,8 +83,15 @@
   </item>
   <item>
     <path>civicrm/dataprocessor/form/output/download</path>
-    <title>Browse Uploaded files</title>
+    <title>Download</title>
     <access_arguments>access uploaded files</access_arguments>
     <page_callback>CRM_DataprocessorOutputExport_Page_Download</page_callback>
   </item>
+  <item>
+    <path>civicrm/dataprocessor/output/export</path>
+    <title>Export</title>
+    <page_callback>CRM_DataprocessorOutputExport_Page_Export</page_callback>
+    <access_callback>1</access_callback>
+    <is_public>true</is_public>
+  </item>
 </menu>
-- 
GitLab