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 109705c..bd987ad 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 dba1cc6..bfe6997 100644 --- a/de.systopia.civiproxy/info.xml +++ b/de.systopia.civiproxy/info.xml @@ -24,4 +24,7 @@ CRM/Civiproxy + + + diff --git a/docs/configuration.md b/docs/configuration.md index a2fe7c5..389705a 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 . '/civicrm/mailing/url/open.php'; ``` 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 7ddeef4..b76370c 100644 --- a/proxy/config.dist.php +++ b/proxy/config.dist.php @@ -41,7 +41,8 @@ $target_civicrm = 'https://your.civicrm.installation.org'; // default paths, override if you want. Set to NULL to disable $target_rest = $target_civicrm . '/sites/all/modules/civicrm/extern/rest.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_mosaico_template_url = NULL; // (disabled by default): $target_civicrm . '/wp-content/uploads/civicrm/ext/uk.co.vedaconsulting.mosaico/packages/mosaico/templates/'; $target_mail_view = $target_civicrm . '/civicrm/mailing/view'; diff --git a/proxy/file.php b/proxy/file.php index 0aa72e8..d51e6d5 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 612688b..3a8c638 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) {