diff --git a/de.systopia.civiproxy/Civi/CiviProxy/CompilerPass.php b/de.systopia.civiproxy/Civi/CiviProxy/CompilerPass.php
new file mode 100644
index 0000000..1c3a3bc
--- /dev/null
+++ b/de.systopia.civiproxy/Civi/CiviProxy/CompilerPass.php
@@ -0,0 +1,26 @@
+
+ * @license AGPL-3.0
+ */
+namespace Civi\CiviProxy;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class CompilerPass implements CompilerPassInterface {
+
+ /**
+ * You can modify the container here before it is dumped to PHP code.
+ */
+ public function process(ContainerBuilder $container) {
+ if (!$container->hasDefinition('data_processor_factory')) {
+ return;
+ }
+ $factoryDefinition = $container->getDefinition('data_processor_factory');
+ $factoryDefinition->addMethodCall('addOutputHandler', array('civiproxy_file_field', 'Civi\CiviProxy\DataProcessor\FileFieldOutputHandler', ts('CiviProxy File download link')));
+ }
+
+
+}
diff --git a/de.systopia.civiproxy/Civi/CiviProxy/DataProcessor/FileFieldOutputHandler.php b/de.systopia.civiproxy/Civi/CiviProxy/DataProcessor/FileFieldOutputHandler.php
new file mode 100644
index 0000000..d4e142b
--- /dev/null
+++ b/de.systopia.civiproxy/Civi/CiviProxy/DataProcessor/FileFieldOutputHandler.php
@@ -0,0 +1,184 @@
+
+ * @license AGPL-3.0
+ */
+
+namespace Civi\CiviProxy\DataProcessor;
+
+use Civi\DataProcessor\FieldOutputHandler\AbstractFieldOutputHandler;
+use Civi\DataProcessor\Source\SourceInterface;
+use Civi\DataProcessor\DataSpecification\FieldSpecification;
+use Civi\DataProcessor\FieldOutputHandler\FieldOutput;
+use Civi\DataProcessor\Exception\DataSourceNotFoundException;
+use Civi\DataProcessor\Exception\FieldNotFoundException;
+
+class FileFieldOutputHandler extends AbstractFieldOutputHandler {
+
+ /**
+ * Returns the data type of this field
+ *
+ * @return String
+ */
+ protected function getType() {
+ return 'String';
+ }
+
+ /**
+ * Returns the formatted value
+ *
+ * @param $rawRecord
+ * @param $formattedRecord
+ *
+ * @return \Civi\DataProcessor\FieldOutputHandler\FieldOutput
+ */
+ public function formatField($rawRecord, $formattedRecord) {
+ $rawValue = $rawRecord[$this->inputFieldSpec->alias];
+ $output = new FieldOutput($rawValue);
+ if ($rawValue) {
+ $proxy_base = \CRM_Core_BAO_Setting::getItem('CiviProxy Settings', 'proxy_url');
+ $attachment = civicrm_api3('Attachment', 'getsingle', array('id' => $rawValue));
+ if (!isset($attachment['is_error']) || $attachment['is_error'] == '0') {
+ $fcs = \CRM_Core_BAO_File::generateFileHash($attachment['entity_id'], $attachment['id']);
+ $output->formattedValue = $proxy_base.'/file.php?id='.$attachment['id'].'&eid='.$attachment['entity_id'].'&fcs='.$fcs;
+ }
+ }
+ return $output;
+ }
+
+ /**
+ * Callback function for determining whether this field could be handled by this output handler.
+ *
+ * @param \Civi\DataProcessor\DataSpecification\FieldSpecification $field
+ * @return bool
+ */
+ public function isFieldValid(FieldSpecification $field) {
+ if ($field->type == 'File') {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @var \Civi\DataProcessor\DataSpecification\FieldSpecification
+ */
+ protected $inputFieldSpec;
+
+ /**
+ * @var \Civi\DataProcessor\DataSpecification\FieldSpecification
+ */
+ protected $outputFieldSpec;
+
+ /**
+ * @var SourceInterface
+ */
+ protected $dataSource;
+
+ /**
+ * @return \Civi\DataProcessor\DataSpecification\FieldSpecification
+ */
+ public function getOutputFieldSpecification() {
+ return $this->outputFieldSpec;
+ }
+
+ /**
+ * Initialize the processor
+ *
+ * @param String $alias
+ * @param String $title
+ * @param array $configuration
+ * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $processorType
+ */
+ public function initialize($alias, $title, $configuration) {
+ $this->dataSource = $this->dataProcessor->getDataSourceByName($configuration['datasource']);
+ if (!$this->dataSource) {
+ throw new DataSourceNotFoundException(ts("Field %1 requires data source '%2' which could not be found. Did you rename or deleted the data source?", array(1=>$title, 2=>$configuration['datasource'])));
+ }
+ $this->inputFieldSpec = $this->dataSource->getAvailableFields()->getFieldSpecificationByName($configuration['field']);
+ if (!$this->inputFieldSpec) {
+ throw new FieldNotFoundException(ts("Field %1 requires a field with the name '%2' in the data source '%3'. Did you change the data source type?", array(
+ 1 => $title,
+ 2 => $configuration['field'],
+ 3 => $configuration['datasource']
+ )));
+ }
+ $this->dataSource->ensureFieldInSource($this->inputFieldSpec);
+
+ $this->outputFieldSpec = clone $this->inputFieldSpec;
+ $this->outputFieldSpec->alias = $alias;
+ $this->outputFieldSpec->title = $title;
+ }
+
+ /**
+ * Returns true when this handler has additional configuration.
+ *
+ * @return bool
+ */
+ public function hasConfiguration() {
+ return true;
+ }
+
+ /**
+ * When this handler has additional configuration you can add
+ * the fields on the form with this function.
+ *
+ * @param \CRM_Core_Form $form
+ * @param array $field
+ */
+ public function buildConfigurationForm(\CRM_Core_Form $form, $field=array()) {
+ $fieldSelect = $this->getFieldOptions($field['data_processor_id']);
+
+ $form->add('select', 'field', ts('Field'), $fieldSelect, true, array(
+ 'style' => 'min-width:250px',
+ 'class' => 'crm-select2 huge data-processor-field-for-name',
+ 'placeholder' => ts('- select -'),
+ ));
+ if (isset($field['configuration'])) {
+ $configuration = $field['configuration'];
+ $defaults = array();
+ if (isset($configuration['field']) && isset($configuration['datasource'])) {
+ $defaults['field'] = $configuration['datasource'] . '::' . $configuration['field'];
+ }
+ $form->setDefaults($defaults);
+ }
+ }
+
+ /**
+ * When this handler has configuration specify the template file name
+ * for the configuration form.
+ *
+ * @return false|string
+ */
+ public function getConfigurationTemplateFileName() {
+ return "CRM/Dataprocessor/Form/Field/Configuration/RawFieldOutputHandler.tpl";
+ }
+
+
+ /**
+ * Process the submitted values and create a configuration array
+ *
+ * @param $submittedValues
+ * @return array
+ */
+ public function processConfiguration($submittedValues) {
+ list($datasource, $field) = explode('::', $submittedValues['field'], 2);
+ $configuration['field'] = $field;
+ $configuration['datasource'] = $datasource;
+ return $configuration;
+ }
+
+ /**
+ * Returns all possible fields
+ *
+ * @param $data_processor_id
+ *
+ * @return array
+ * @throws \Exception
+ */
+ protected function getFieldOptions($data_processor_id) {
+ $fieldSelect = \CRM_Dataprocessor_Utils_DataSourceFields::getAvailableFieldsInDataSources($data_processor_id, array($this, 'isFieldValid'));
+ return $fieldSelect;
+ }
+
+
+}
diff --git a/de.systopia.civiproxy/civiproxy.php b/de.systopia.civiproxy/civiproxy.php
index ec2c0c1..ebc9745 100644
--- a/de.systopia.civiproxy/civiproxy.php
+++ b/de.systopia.civiproxy/civiproxy.php
@@ -10,6 +10,8 @@
require_once 'civiproxy.civix.php';
+use \Symfony\Component\DependencyInjection\ContainerBuilder;
+
/**
* We will provide our own Mailer (wrapping the original one).
* so we can mend all the URLs in outgoing emails
@@ -18,6 +20,15 @@ function civiproxy_civicrm_alterMailer(&$mailer, $driver, $params) {
$mailer = new CRM_Civiproxy_Mailer($mailer);
}
+/**
+ * Implements hook_civicrm_container()
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_container/
+ */
+function civiproxy_civicrm_container(ContainerBuilder $container) {
+ $container->addCompilerPass(new Civi\CiviProxy\CompilerPass());
+}
+
/**
* Implementation of hook_civicrm_config
*/
diff --git a/de.systopia.civiproxy/info.xml b/de.systopia.civiproxy/info.xml
index 26bac30..7ebf4a6 100644
--- a/de.systopia.civiproxy/info.xml
+++ b/de.systopia.civiproxy/info.xml
@@ -19,4 +19,7 @@
CRM/Civiproxy
+
+
+
diff --git a/docs/configuration.md b/docs/configuration.md
index 83fd61d..385ef82 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -50,7 +50,7 @@ First thing you need to configure is the base URL of your CiviProxy server using
```
!!! note
- This guide assumes a Drupal7 target CiviCRM with clean URLs enabled. If this is not the case for you, you might have to adjust the URLs and/or encounter issues. If so, please report on GitHub!
+ This guide assumes a Drupal7 target CiviCRM with clean URLs enabled. If this is not the case for you, you might have to adjust the URLs and/or encounter issues. If so, please report on GitHub!
### Configuring the link to the secure target CiviCRM
@@ -75,12 +75,16 @@ $target_open = $target_civicrm . '/sites/all/modules/civicrm/extern/open.ph
```
If you set it to the value NULL this functionality will not be available on your CiviProxy server.
### Setting for the location of images and included files in your mail(ing)
-CiviCRM stores images and attachments you include in your (bulk) mail in a specific folder. In CiviProxy the name of this folder is stored in variable `$target_file` in the `config.php` file:
+CiviCRM stores images and attachments you include in your (bulk) mail in a specific folder. In CiviProxy the name of this folder is stored in variable `$target_static_file` in the `config.php` file:
```php
-$target_file = $target_civicrm . '/sites/default/files/civicrm/persist/';
+$target_download_file = $target_civicrm . '/civicrm/file';
+$target_static_file = $target_civicrm . '/sites/default/files/civicrm/persist/';
```
If you set it to the value NULL this functionality will not be available on your CiviProxy server.
+The `$target_download_file` is used for downloading files from custom file fields (or the contact image).
+The `$target_static_file` is used for downloading images in mailings.
+
!!! note
By default CiviProxy will cache the files so it does not have to file from CiviCRM for each individual mail that is part of a bulk mailing. The default settings can be found in the `config.php` file:
```php
diff --git a/proxy/config.dist.php b/proxy/config.dist.php
index 59510bb..eaaac5e 100644
--- a/proxy/config.dist.php
+++ b/proxy/config.dist.php
@@ -35,7 +35,8 @@ $target_civicrm = 'https://your.civicrm.installation.org';
$target_rest = $target_civicrm . '/sites/all/modules/civicrm/extern/rest.php';
$target_url = $target_civicrm . '/sites/all/modules/civicrm/extern/url.php';
$target_open = $target_civicrm . '/sites/all/modules/civicrm/extern/open.php';
-$target_file = $target_civicrm . '/sites/default/files/civicrm/persist/';
+$target_download_file = $target_civicrm . '/civicrm/file';
+$target_static_file = $target_civicrm . '/sites/default/files/civicrm/persist/';
$target_mosaico = NULL; // (disabled by default): $target_civicrm . '/civicrm/mosaico/img?src=';
$target_mail_view = $target_civicrm . '/civicrm/mailing/view';
@@ -155,4 +156,4 @@ $_webhook2api = [
"parameter_sanitation" => [],
]
]
-];
\ No newline at end of file
+];
diff --git a/proxy/file.php b/proxy/file.php
index 768072f..ddd9ab0 100644
--- a/proxy/file.php
+++ b/proxy/file.php
@@ -10,38 +10,51 @@
require_once "config.php";
require_once "proxy.php";
-// see if file caching is enabled
-if (!$target_file) civiproxy_http_error("Feature disabled", 405);
-
// basic check
civiproxy_security_check('file');
// basic restraints
-$valid_parameters = array( 'id' => 'string' );
+$valid_parameters = array(
+ 'id' => 'string',
+ 'eid' => 'int',
+ 'fcs' => 'string'
+);
$parameters = civiproxy_get_parameters($valid_parameters);
// check if id specified
if (empty($parameters['id'])) civiproxy_http_error("Resource not found");
-// check restrictions
-if (!empty($file_cache_exclude)) {
- foreach ($file_cache_exclude as $pattern) {
- if (preg_match($pattern, $parameters['id'])) {
+$static_file = true;
+if (isset($parameters['eid']) && isset($parameters['fcs'])) {
+ $static_file = false;
+ // see if file caching is enabled
+ if (!$target_download_file) civiproxy_http_error("Feature disabled", 405);
+} else {
+ // see if file caching is enabled
+ if (!$target_static_file && isset($target_file)) {
+ $target_static_file = $target_file; // Backwards compatibility.
+ }
+ if (!$target_static_file) civiproxy_http_error("Feature disabled", 405);
+ // check restrictions
+ if (!empty($file_cache_exclude)) {
+ foreach ($file_cache_exclude as $pattern) {
+ if (preg_match($pattern, $parameters['id'])) {
+ civiproxy_http_error("Invalid Resource", 403);
+ }
+ }
+ }
+ if (!empty($file_cache_include)) {
+ $accept_id = FALSE;
+ foreach ($file_cache_include as $pattern) {
+ if (preg_match($pattern, $parameters['id'])) {
+ $accept_id = TRUE;
+ }
+ }
+ if (!$accept_id) {
civiproxy_http_error("Invalid Resource", 403);
}
}
}
-if (!empty($file_cache_include)) {
- $accept_id = FALSE;
- foreach ($file_cache_include as $pattern) {
- if (preg_match($pattern, $parameters['id'])) {
- $accept_id = TRUE;
- }
- }
- if (!$accept_id) {
- civiproxy_http_error("Invalid Resource", 403);
- }
-}
// load PEAR file cache
ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . 'libs');
@@ -52,6 +65,10 @@ $file_cache = new Cache_Lite($file_cache_options);
// look up the required resource
$header_key = 'header&' . $parameters['id'];
$data_key = 'data&' . $parameters['id'];
+if (!$static_file) {
+ $header_key .= '&eid='.$parameters['eid'];
+ $data_key .= '&eid='.$parameters['eid'];
+}
$header = $file_cache->get($header_key);
$data = $file_cache->get($data_key);
@@ -68,7 +85,10 @@ if ($header && $data) {
}
// if we get here, we have a cache miss => load
-$url = $target_file . $parameters['id'];
+$url = $target_static_file . $parameters['id'];
+if (!$static_file) {
+ $url = $target_download_file .'?reset=1&id='.$parameters['id'].'&eid='.$parameters['eid'].'&fcs='.$parameters['fcs'];
+}
// error_log("CACHE MISS. LOADING $url");
$curlSession = curl_init();
@@ -100,12 +120,28 @@ $body = $content[1];
// extract headers
$header_lines = explode(chr(10), $header);
+// Check whether the Content-Disposition header is available but only when it is
+// a dynamic file.
+$content_disposition_header_present = FALSE;
+foreach ($header_lines as $header_line) {
+ if (stripos($header_line, 'Content-Disposition')===0) {
+ $content_disposition_header_present = TRUE;
+ }
+}
+if (!$static_file && !$content_disposition_header_present) {
+ // check whether the content disposition header is available.
+ // If not we are dealing with an invalid file.
+ // And CiviCRM does a redirect to the login page however we
+ // dont want to expose that through CiviProxy so we will return an error message instead.
+ civiproxy_http_error("Invalid Resource", 403);
+}
// store the information in the cache
$file_cache->save(json_encode($header_lines), $header_key);
$file_cache->save($body, $data_key);
// and reply
+$content_disposition_header_present = FALSE;
foreach ($header_lines as $header_line) {
header($header_line);
}
diff --git a/proxy/proxy.php b/proxy/proxy.php
index b031aee..d69ae30 100644
--- a/proxy/proxy.php
+++ b/proxy/proxy.php
@@ -96,7 +96,7 @@ function civiproxy_redirect($url_requested, $parameters) {
* so they will point to this proxy instead
*/
function civiproxy_mend_URLs(&$string) {
- global $target_rest, $target_url, $target_open, $target_file, $target_mail, $proxy_base, $target_mosaico, $target_civicrm;
+ global $target_rest, $target_url, $target_open, $target_static_file, $target_mail, $proxy_base, $target_mosaico, $target_civicrm;
if ($target_rest) {
$string = preg_replace("#{$target_rest}#", $proxy_base . '/rest.php', $string);
@@ -110,8 +110,8 @@ function civiproxy_mend_URLs(&$string) {
if ($target_mail) {
$string = preg_replace("#{$target_mail}#", $proxy_base . '/mail.php', $string);
}
- if ($target_file) {
- $string = preg_replace("#{$target_file}#", $proxy_base . '/file.php?id=', $string);
+ if ($target_static_file) {
+ $string = preg_replace("#{$target_static_file}#", $proxy_base . '/file.php?id=', $string);
// https://github.com/systopia/CiviProxy/issues/38
// fix for relative
if ($target_mosaico) {