classes/Gems/Export/ExportAbstract.php
<?php
/**
*
* @package Gems
* @subpackage Export
* @copyright Copyright (c) 2015 Erasmus MC
* @license New BSD License
*/
namespace Gems\Export;
/**
*
* @package Gems
* @subpackage Export
* @copyright Copyright (c) 2015 Erasmus MC
* @license New BSD License
* @since Class available since version 1.7.1
*/
abstract class ExportAbstract extends \MUtil_Translate_TranslateableAbstract implements ExportInterface
{
/**
* @var \Zend_Session_Namespace Own session used for non-batch exports
*/
protected $_session;
/**
* @var \Gems_Task_TaskRunnerBatch The batch object if one is set
*/
protected $batch;
/**
* @var array Data submitted by export form
*/
protected $data;
/**
* @var array Array with the filter options that should be used for this exporter
*/
protected $defaultModelFilterAttributes = ['multiOptions', 'formatFunction', 'dateFormat', 'storageFormat', 'itemDisplay'];
/**
* @var string The temporary filename while the file is being written
*/
protected $tempFilename;
/**
* @var string Current used file extension
*/
protected $fileExtension;
/**
* @var string The export file name, how it should be downloaded
*/
protected $filename;
/**
* @var array Array of all the filenames, new_name => temp_name
*/
protected $files;
/**
* @var array Model filters for export
*/
protected $filter;
/**
* @var array Array of the loaded first row of the model
*/
protected $firstRow;
/**
* @var array Array with the filter options that should be used for this exporter
*/
protected $modelFilterAttributes = array('multiOptions', 'formatFunction', 'dateFormat', 'storageFormat', 'itemDisplay');
/**
*
* @var integer Model Id for when multiple models are passed
*/
protected $modelId;
/**
* @var \Gems_Loader
*/
public $loader;
/**
* @var \MUtil_Model_ModelAbstract Current model to export
*/
protected $model;
/**
* @var array Filter settings of the current loaded model
*/
protected $modelFilter;
/**
* @var integer How many rows the batch will do in one go
*/
protected $rowsPerBatch = 500;
/**
* @return string name of the specific export
*/
abstract public function getName();
/**
* form elements for extra options for this particular export option
* @param \MUtil_Form $form Current form to add the form elements
* @param array $data current options set in the form
* @return array Form elements
*/
public function getFormElements(&$form, &$data) {}
/**
* @return string|null Optional snippet containing help text
*/
public function getHelpSnippet()
{
return null;
}
/**
* Returns an array of ordered columnnames that have a label
*
* @return array Array of columnnames
*/
public function getLabeledColumns()
{
if (!$this->model->hasMeta('labeledColumns')) {
$orderedCols = $this->model->getItemsOrdered();
$results = array();
foreach ($orderedCols as $name) {
if ($this->model->has($name, 'label')) {
$results[] = $name;
}
}
$this->model->setMeta('labeledColumns', $results);
}
return $this->model->getMeta('labeledColumns');
}
/**
* @return array Default values in form
*/
public function getDefaultFormValues() {
return [];
}
/**
* Add an export command with specific details. Can be batched.
* @param array $data Data submitted by export form
* @param array $modelId Model Id when multiple models are passed
*/
public function addExport($data, $modelId = false)
{
$this->files = $this->getFiles();
$this->data = $data;
$this->modelId = $modelId;
if ($model = $this->getModel()) {
$totalRows = $this->getModelCount();
$this->addFile();
$this->addHeader($this->tempFilename . $this->fileExtension);
$currentRow = 0;
do {
$filter['limit'] = array($this->rowsPerBatch, $currentRow);
if ($this->batch) {
$this->batch->addTask('Export_ExportCommand', $data['type'], 'addRows', $data, $modelId, $this->tempFilename, $filter);
} else {
$this->addRows($data, $modelId, $this->tempFilename, $filter);
}
$currentRow = $currentRow + $this->rowsPerBatch;
} while ($currentRow < $totalRows);
if ($this->batch) {
$this->batch->addTask('Export_ExportCommand', $data['type'], 'addFooter', $this->tempFilename . $this->fileExtension, $modelId, $data);
$this->batch->setSessionVariable('files', $this->files);
} else {
$this->addFooter($this->tempFilename . $this->fileExtension, $modelId, $data);
$this->_session->files = $this->files;
}
}
}
/**
* Creates a new file and adds it to the files array
*/
protected function addFile()
{
$exportTempDir = $this->getExportTempDir();
if (! is_dir($exportTempDir)) {
\MUtil_File::ensureDir($exportTempDir);
}
$tempFilename = $exportTempDir . 'export-' . md5(time() . rand());
$this->tempFilename = $tempFilename;
$basename = $this->cleanupName($this->model->getName());
$filename = $basename;
$i = 1;
while (isset($this->files[$filename . $this->fileExtension])) {
$filename = $basename . '_' . $i;
$i++;
}
$this->filename = $filename;
$this->files[$filename . $this->fileExtension] = $tempFilename . $this->fileExtension;
$file = fopen($tempFilename . $this->fileExtension, 'w');
fclose($file);
}
/**
* Add headers to a specific file
* @param string $filename The temporary filename while the file is being written
*/
abstract protected function addHeader($filename);
/**
* Add model rows to file. Can be batched
* @param array $data Data submitted by export form
* @param array $modelId Model Id when multiple models are passed
* @param string $tempFilename The temporary filename while the file is being written
* @param array $filter Filter (limit) to use
*/
public function addRows($data, $modelId, $tempFilename, $filter)
{
$this->data = $data;
$this->modelId = $modelId;
$this->model = $this->getModel();
$this->model->setFilter($filter + $this->model->getFilter());
if ($this->model) {
$rows = $this->model->load();
$file = fopen($tempFilename . $this->fileExtension, 'a');
foreach ($rows as $row) {
$this->addRow($row, $file);
}
fclose($file);
}
}
public function afterRegistry() {
parent::afterRegistry();
if (!$this->batch) {
$this->_session = new \Zend_Session_Namespace(__CLASS__);
}
}
/**
* Add a separate row to a file
* @param array $row a row in the model
* @param file $file The already opened file
*/
abstract public function addRow($row, $file);
/**
* Add a footer to a specific file
* @param string $filename The temporary filename while the file is being written
*/
/**
* Add a footer to a specific file
* @param string $filename The temporary filename while the file is being written
* @param string $modelId ID of the current model
* @param array $data Current export settings
*/
public function addFooter($filename, $modelId = null, $data = null) {
$this->modelId = $modelId;
}
/**
* Clean a proposed filename up so it can be used correctly as a filename
* @param string $filename Proposed filename
* @return string filtered filename
*/
protected function cleanupName($filename)
{
$filename = str_replace(array('/', '\\', ':', ' '), '_', $filename);
// Remove dot if it starts with one
$filename = trim($filename, '.');
return \MUtil_File::cleanupName($filename);
}
/**
* Single point for mitigating csv injection vulnerabilities
*
* https://www.owasp.org/index.php/CSV_Injection
*
* @param string $input
* @return string
*/
protected function filterCsvInjection($input)
{
// Try to prevent csv injection
$dangers = ['=', '+', '-', '@'];
// Trim leading spaces for our test
$trimmed = trim($input);
if (strlen($trimmed)>1 && in_array($trimmed[0], $dangers)) {
return "'" . $input;
} else {
return $input;
}
}
protected function filterDateFormat($value, $dateFormat, $columnName)
{
$storageFormat = $this->model->get($columnName, 'storageFormat');
return \MUtil_Date::format($value, $dateFormat, $storageFormat);
}
protected function filterFormatFunction($value, $functionName)
{
if (is_string($functionName) && method_exists($this, $functionName)) {
return call_user_func(array($this, $functionName), $value);
} else {
return call_user_func($functionName, $value);
}
}
protected function filterHtml($result)
{
if ($result instanceof \MUtil_Html_ElementInterface && !($result instanceof \MUtil_Html_Sequence)) {
if ($result instanceof \MUtil_Html_AElement) {
$href = $result->href;
$result = $href;
} elseif ($result->count() > 0) {
$result = $result[0];
}
}
if (is_object($result)) {
// If it is Lazy, execute it
if ($result instanceof \MUtil_Lazy_LazyInterface) {
$result = \MUtil_Lazy::rise($result);
}
// If it is Html, render it
if ($result instanceof \MUtil_Html_HtmlInterface) {
$viewRenderer = \Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
if (null === $viewRenderer->view) {
$viewRenderer->initView();
}
$view = $viewRenderer->view;
$result = $result->render($view);
}
}
return $result;
}
protected function filterItemDisplay($value, $functionName)
{
if (is_callable($functionName)) {
$result = call_user_func($functionName, $value);
} elseif (is_object($functionName)) {
if (($functionName instanceof \MUtil_Html_ElementInterface) || method_exists($functionName, 'append')) {
$object = clone $functionName;
$result = $object->append($value);
}
} elseif (is_string($functionName)) {
// Assume it is a html tag when a string
$result = \MUtil_Html::create($functionName, $value);
}
return $result;
}
protected function filterMultiOptions($result, $multiOptions)
{
if (is_array($multiOptions)) {
/*
* Sometimes a field is an array and will be formatted later on using the
* formatFunction -> handle each element in the array.
*/
if (is_array($result)) {
foreach ($result as $key => $value) {
if (array_key_exists($value, $multiOptions)) {
$result[$key] = $multiOptions[$value];
}
}
} else {
if (array_key_exists($result, $multiOptions)) {
$result = $multiOptions[$result];
}
}
}
return $result;
}
/**
* Filter the data in a row so that correct values are being used
* @param array $row a row in the model
* @return array The filtered row
*/
protected function filterRow($row)
{
$exportRow = array();
foreach ($row as $columnName => $result) {
if (!is_null($this->model->get($columnName, 'label'))) {
$options = $this->model->get($columnName, $this->modelFilterAttributes);
foreach ($options as $optionName => $optionValue) {
switch ($optionName) {
case 'dateFormat':
// if there is a formatFunction skip the date formatting
if (array_key_exists('formatFunction', $options)) {
continue 2;
}
$result = $this->filterDateFormat($result, $optionValue, $columnName);
break;
case 'formatFunction':
$result = $this->filterFormatFunction($result, $optionValue);
break;
case 'itemDisplay':
$result = $this->filterItemDisplay($result, $optionValue);
break;
case 'multiOptions':
$result = $this->filterMultiOptions($result, $optionValue);
break;
default:
break;
}
}
if ($result instanceof \MUtil_Date) {
$result = $this->filterDateFormat($result, 'yyyy-MM-dd HH:mm:ss', $columnName);
}
$result = $this->filterHtml($result);
$exportRow[$columnName] = $result;
}
}
return $exportRow;
}
/**
* Finalizes the files stored in $this->files.
* If it has 1 file, it will return that file, if it has more, it will return a zip containing all the files, named as the first file in the array.
* @param array $data Current export settings
* @return array File with download headers
*/
public function finalizeFiles($data=null)
{
$this->getFiles();
if (count($this->files) === 0) {
return false;
}
$firstName = key($this->files);
$file = array();
if (count($this->files) === 1) {
$firstFile = $this->files[$firstName];
$file['file'] = $firstFile;
$file['headers'][] = "Content-Type: application/download";
$file['headers'][] = "Content-Disposition: attachment; filename=\"" . $firstName . "\"";
} elseif (count($this->files) >= 1) {
$nameArray = explode('.', $firstName);
array_pop($nameArray);
$filename = join('.', $nameArray) . '.zip';
$zipFile = dirname($this->files[$firstName]) . '/export-' . md5(time() . rand()) . '.zip';
$zipArchive = new \ZipArchive();
$zipArchive->open($zipFile, \ZipArchive::CREATE);
foreach ($this->files as $newName => $tempName) {
$zipArchive->addFile($tempName, $newName);
}
$zipArchive->close();
foreach ($this->files as $tempName) {
if (file_exists($tempName)) {
unlink($tempName);
}
}
$file = array();
$file['file'] = $zipFile;
$file['headers'][] = "Content-Type: application/download";
$file['headers'][] = "Content-Disposition: attachment; filename=\"" . $filename . "\"";
}
$file['headers'][] = "Expires: Mon, 26 Jul 1997 05:00:00 GMT"; // Date in the past
$file['headers'][] = "Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT";
$file['headers'][] = "Cache-Control: must-revalidate, post-check=0, pre-check=0";
$file['headers'][] = "Pragma: cache"; // HTTP/1.0
if ($this->batch) {
$this->batch->setSessionVariable('file', $file);
} else {
return $file;
}
}
/**
* Return the answermodel for the given filter
*
* @param array $filter
* @param array $data
* @param array|string $sort
* @return \MUtil_Model_ModelAbstract model
*/
protected function getAnswerModel($exportModelSource, array $filter, array $data, $sort)
{
$exportModelSource = $this->loader->getExportModelSource($exportModelSource);
$model = $exportModelSource->getModel($filter, $data);
$noExportColumns = $model->getColNames('noExport');
foreach($noExportColumns as $colName) {
$model->remove($colName, 'label');
}
$model->applyParameters($filter, true);
$model->addSort($sort);
return $model;
}
protected function getExportTempDir()
{
return GEMS_ROOT_DIR . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
}
/**
* Returns the files array. It might be stored in the batch session or normal session.
* @return array Files array
*/
protected function getFiles()
{
if (!$this->files) {
$files = array();
if ($this->batch) {
$files = $this->batch->getSessionVariable('files');
} else {
$files = $this->_session->files;
}
if (!is_array($files)) {
$files = array();
}
$this->files = $files;
}
return $this->files;
}
/**
* Get the model to export
* @return \MUtil_Model_ModelAbstract
*/
public function getModel()
{
if ($this->batch) {
$model = $this->batch->getVariable('model');
} else {
$model = $this->_session->model;
}
if (is_array($model)) {
if ($this->modelId) {
if (isset($model[$this->modelId]) && $model[$this->modelId] instanceof \MUtil_Model_ModelAbstract) {
$model = $model[$this->modelId];
} else {
$modelType = null;
$filter = null;
$data = null;
$sort = null;
$extra = null;
if (isset($model[$this->modelId], $model[$this->modelId]['model'])) {
$modelType = $model[$this->modelId]['model'];
} elseif (isset($model['model'])) {
$modelType = $model['model'];
}
if (isset($model[$this->modelId], $model[$this->modelId]['filter'])) {
$filter = $model[$this->modelId]['filter'];
} elseif (isset($model['filter'])) {
$filter = $model['filter'];
}
if (isset($model[$this->modelId], $model[$this->modelId]['data'])) {
$data = $model[$this->modelId]['data'];
} elseif (isset($model['data'])) {
$data = $model['data'];
}
if (isset($model[$this->modelId], $model[$this->modelId]['sort'])) {
$sort = $model[$this->modelId]['sort'];
} elseif (isset($model['sort'])) {
$sort = $model['sort'];
}
if (isset($model[$this->modelId], $model[$this->modelId]['extra'])) {
$extra = $model[$this->modelId]['extra'];
} elseif (isset($model['extra'])) {
$extra = $model['extra'];
}
if ($modelType && is_callable($modelType)) {
$model = $modelType($this->loader, $filter, $data, $sort, $extra);
} else {
$exportModelSource = 'AnswerExportModelSource';
if (isset($model[$this->modelId]['exportModelSource'])) {
$exportModelSource = $model[$this->modelId]['exportModelSource'];
} elseif (isset($model['exportModelSource'])) {
$exportModelSource = $model['exportModelSource'];
}
$filter['gto_id_survey'] = $this->modelId;
$model = $this->getAnswerModel($exportModelSource, $filter, $data, $sort);
}
}
} else {
return false;
}
}
$this->model = $model;
if ($this->model->getMeta('exportPreprocess') === null) {
$this->preprocessModel();
$this->model->setMeta('exportPreprocess', true);
}
return $this->model;
}
/**
* Get the number of items in a specific model, using the models paginator
* @param array $filter Filter for the model
* @return int Number of items in the model
*/
protected function getModelCount($filter = true)
{
if ($this->model && $this->model instanceof \MUtil_Model_ModelAbstract) {
$totalCount = $this->model->loadPaginator()->getTotalItemCount();
return $totalCount;
}
return 0;
}
/**
* Preprocess the model to add specific options
*/
protected function preprocessModel()
{
$this->getLabeledColumns();
}
/**
* Set the batch to be used by this source
*
* Use $this->hasBatch to check for existence
*
* @param \Gems_Task_TaskRunnerBatch $batch
*/
public function setBatch(\Gems_Task_TaskRunnerBatch $batch)
{
$this->batch = $batch;
}
/**
* Set the model when not in batch mode
*
* @param \MUtil_Model_ModelAbstract $model
*/
public function setModel(\MUtil_Model_ModelAbstract $model)
{
if ($this->_session) {
$this->_session->model = $model;
}
}
}