Tinebase_Export - introduce definition templateFileId, add vfs access check
[tine20] / tine20 / Tinebase / Export / Abstract.php
1 <?php
2 /**
3  * Tinebase Abstract export class
4  *
5  * @package     Tinebase
6  * @subpackage  Export
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Paul Mehrer <p.mehrer@metaways.de>
9  * @copyright   Copyright (c) 2017-2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * Tinebase Abstract export class
15  *
16  * @package     Tinebase
17  * @subpackage    Export
18  *
19  */
20 abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInterface
21 {
22     const ROW_TYPE_RECORD = 'record';
23     const ROW_TYPE_GENERIC_HEADER = 'genericHeader';
24     const ROW_TYPE_GROUP_HEADER = 'groupHeader';
25
26     /**
27      * default export definition name
28      *
29      * @var string
30      */
31     protected $_defaultExportname = 'default';
32
33     /**
34      * the record controller
35      *
36      * @var Tinebase_Controller_Record_Abstract
37      */
38     protected $_controller = null;
39
40     /**
41      * translation object
42      *
43      * @var Zend_Translate
44      */
45     protected $_translate;
46
47     /**
48      * locale object
49      *
50      * @var Zend_Locale
51      */
52     protected $_locale;
53
54     /**
55      * export config
56      *
57      * @var Zend_Config_Xml
58      */
59     protected $_config = array();
60
61     /**
62      * @var string application name of this export class
63      */
64     protected $_applicationName = null;
65
66     /**
67      * the record model
68      *
69      * @var string
70      */
71     protected $_modelName = null;
72
73     /**
74      * filter to generate export for
75      *
76      * @var Tinebase_Model_Filter_FilterGroup
77      */
78     protected $_filter = null;
79
80     /**
81      * sort records by this field (array keys: sort / dir / ...)
82      *
83      * @var array
84      * @see Tinebase_Model_Pagination
85      */
86     protected $_sortInfo = array();
87
88     /**
89      * preference key if users can have different export configs
90      *
91      * @var string
92      */
93     protected $_prefKey = null;
94
95     /**
96      * format strings
97      *
98      * @var string
99      */
100     protected $_format = null;
101
102     /**
103      * custom field names for this model
104      *
105      * @var array
106      */
107     protected $_customFieldNames = null;
108
109     /**
110      * user fields to resolve
111      *
112      * @var array
113      */
114     protected $_userFields = array('created_by', 'last_modified_by', 'account_id');
115
116     /**
117      * first iteration (helper to write generic headings, etc.)
118      *
119      * @var boolean
120      */
121     protected $_firstIteration = true;
122
123     /**
124      * helper to determine if we are done with record processing
125      *
126      * @var bool
127      */
128     protected $_iterationDone = false;
129
130     /**
131      * just dump all properties of the records to _writeValue (through _getValue($field, $record) of course)
132      *
133      * @var boolean
134      */
135     protected $_dumpRecords = true;
136
137     /**
138      * write a generic header based on the properties of a record created from _modelName
139      *
140      * @var boolean
141      */
142     protected $_writeGenericHeader = true;
143
144     /**
145      * class cache for field config from _config->columns->column
146      *
147      * @var array
148      */
149     protected $_fieldConfig = array();
150
151     /**
152      * fields with special treatment in addBody
153      *
154      * @var array
155      */
156     protected $_specialFields = array();
157
158     /**
159      * if set to true _hasTwig() will return true in any case
160      *
161      * @var boolean
162      */
163     protected $_hasTemplate = false;
164
165     /** @var Twig_Environment */
166     protected $_twigEnvironment = null;
167     /**
168      * @var Twig_TemplateWrapper|null
169      */
170     protected $_twigTemplate = null;
171
172     protected $_twigMapping = array();
173
174     /**
175      * @var string
176      */
177     protected $_templateFileName = null;
178
179     protected $_resolvedFields = array();
180
181     /**
182      * @var Tinebase_DateTime|null
183      */
184     protected $_exportTimeStamp = null;
185
186     /**
187      * @var null|string
188      */
189     protected $_logoPath = null;
190
191     /**
192      * @var Tinebase_Record_RecordSet|null
193      */
194     protected $_records = null;
195
196     protected $_lastGroupValue = null;
197
198     protected $_groupByProperty = null;
199
200     protected $_groupByProcessor = null;
201
202     protected $_currentRowType = null;
203
204     /**
205      * @var Tinebase_Record_Abstract
206      */
207     protected $_currentRecord = null;
208
209     protected $_getRelations = false;
210
211     protected $_additionalRecords = array();
212
213     protected $_keyFields = array();
214
215     protected $_virtualFields = array();
216
217     protected $_foreignIdFields = array();
218
219     /**
220      * the constructor
221      *
222      * @param Tinebase_Model_Filter_FilterGroup $_filter
223      * @param Tinebase_Controller_Record_Interface $_controller (optional)
224      * @param array $_additionalOptions (optional) additional options
225      */
226     public function __construct(
227         Tinebase_Model_Filter_FilterGroup $_filter,
228         Tinebase_Controller_Record_Interface $_controller = null,
229         $_additionalOptions = array()
230     ) {
231         $this->_filter = $_filter;
232         if (! $this->_modelName) {
233             $this->_modelName = $this->_filter->getModelName();
234         }
235         if (! $this->_applicationName) {
236             $this->_applicationName = $this->_filter->getApplicationName();
237         }
238
239         $this->_controller = ($_controller !== null) ? $_controller :
240             Tinebase_Core::getApplicationInstance($this->_applicationName, $this->_modelName);
241         $this->_translate = Tinebase_Translation::getTranslation($this->_applicationName);
242         $this->_locale = Tinebase_Core::get(Tinebase_Core::LOCALE);
243         $this->_config = $this->_getExportConfig($_additionalOptions);
244         if ($this->_config->template) {
245             $this->_templateFileName = $this->_config->template;
246         }
247         if ($this->_config->templateFileId) {
248             try {
249                 $path = Tinebase_Model_Tree_Node_Path::createFromStatPath(Tinebase_FileSystem::getInstance()->getPathOfNode($this->_config->templateFileId, true));
250                 $this->_templateFileName = $path->streamwrapperpath;
251             } catch (Exception $e) {}
252         }
253         if (isset($_additionalOptions['template'])) {
254             try {
255                 $path = Tinebase_Model_Tree_Node_Path::createFromStatPath(Tinebase_FileSystem::getInstance()->getPathOfNode($_additionalOptions['template'], true));
256                 $this->_templateFileName = $path->streamwrapperpath;
257             } catch (Exception $e) {}
258         }
259         if (! $this->_modelName && !empty($this->_config->model)) {
260             $this->_modelName = $this->_config->model;
261         }
262         $this->_exportTimeStamp = Tinebase_DateTime::now();
263
264         if (!empty($this->_config->group)) {
265             $this->_groupByProperty = $this->_config->group;
266             $this->_sortInfo['sort'] = $this->_groupByProperty;
267             if (!empty($this->_config->groupSortDir)) {
268                 $this->_sortInfo['dir'] = $this->_config->groupSortDir;
269             }
270         }
271
272         if (isset($_additionalOptions['sortInfo'])) {
273             if (isset($this->_sortInfo['sort'])) {
274                 $this->_sortInfo['sort'] = array_unique(array_merge((array)$this->_sortInfo['sort'],
275                     (array)((isset($_additionalOptions['sortInfo']['field']) ?
276                         $_additionalOptions['sortInfo']['field'] : $_additionalOptions['sortInfo']['sort']))));
277             } else {
278                 if (isset($_additionalOptions['sortInfo']['field'])) {
279                     $this->_sortInfo['sort'] = $_additionalOptions['sortInfo']['field'];
280                     $this->_sortInfo['dir'] = isset($_additionalOptions['sortInfo']['direction']) ?
281                         $_additionalOptions['sortInfo']['direction'] : 'ASC';
282                 } else {
283                     $this->_sortInfo = $_additionalOptions['sortInfo'];
284                 }
285             }
286         }
287
288         if (isset($_additionalOptions['recordData'])) {
289             if (isset($_additionalOptions['recordData']['container_id']) && is_array($_additionalOptions['recordData']['container_id'])) {
290                 $_additionalOptions['recordData']['container_id'] = $_additionalOptions['recordData']['container_id']['id'];
291             }
292             $this->_records = new Tinebase_Record_RecordSet($this->_modelName,
293                 array(new $this->_modelName($_additionalOptions['recordData'])));
294         }
295
296         if (isset($_additionalOptions['additionalRecords'])) {
297             foreach ($_additionalOptions['additionalRecords'] as $key => $value) {
298                 if (!isset($value['model']) || !isset($value['recordData'])) {
299                     throw new Tinebase_Exception_UnexpectedValue('additionalRecords needs to specify model and recordData');
300                 }
301                 $record = new $value['model']($value['recordData']);
302                 $this->_additionalRecords[$key] = $record;
303             }
304         }
305
306         if ($this->_config->keyFields) {
307             foreach ($this->_config->keyFields as $keyFields) {
308                 if ($keyFields->propertyName) {
309                     $keyFields = array($keyFields);
310                 }
311                 foreach($keyFields as $keyField) {
312                     $this->_keyFields[$keyField->propertyName] = $keyField->name;
313                 }
314             }
315         }
316
317         if ($this->_config->foreignIds) {
318             foreach ($this->_config->foreignIds as $foreignIds) {
319                 if ($foreignIds->controller) {
320                     $foreignIds = array($foreignIds);
321                 }
322                 foreach($foreignIds as $foreignId) {
323                     $this->_foreignIdFields[$foreignId->name] = $foreignId->controller;
324                 }
325             }
326         }
327
328         if ($this->_config->virtualFields) {
329             foreach ($this->_config->virtualFields as $virtualFields) {
330                 if ($virtualFields->relatedModel) {
331                     $virtualFields = array($virtualFields);
332                 }
333                 foreach($virtualFields as $virtualField) {
334                     $this->_virtualFields[$virtualField->name] = array(
335                         'relatedModel' => $virtualField->relatedModel,
336                         'relatedDegree' => $virtualField->relatedDegree,
337                         'type' => $virtualField->type
338                     );
339                 }
340             }
341         }
342     }
343
344     /**
345      * get export config
346      *
347      * @param array $_additionalOptions additional options
348      * @return Zend_Config_Xml
349      * @throws Tinebase_Exception_NotFound
350      */
351     protected function _getExportConfig($_additionalOptions = array())
352     {
353         if ((isset($_additionalOptions['definitionFilename']) ||
354             array_key_exists('definitionFilename', $_additionalOptions))) {
355             // get definition from file
356             $definition = Tinebase_ImportExportDefinition::getInstance()->getFromFile(
357                 $_additionalOptions['definitionFilename'],
358                 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)->getId()
359             );
360         } elseif ((isset($_additionalOptions['definitionId']) ||
361             array_key_exists('definitionId', $_additionalOptions))) {
362             $definition = Tinebase_ImportExportDefinition::getInstance()->get($_additionalOptions['definitionId']);
363         } else {
364             // get preference from db and set export definition name
365             $exportName = $this->_defaultExportname;
366             if ($this->_prefKey !== null) {
367                 $exportName = Tinebase_Core::getPreference($this->_applicationName)->
368                     getValue($this->_prefKey, $exportName);
369             }
370
371             // get export definition by name / model
372             $filter = new Tinebase_Model_ImportExportDefinitionFilter(array(
373                 array('field' => 'model', 'operator' => 'equals', 'value' => $this->_modelName),
374                 array('field' => 'name',  'operator' => 'equals', 'value' => $exportName),
375             ));
376             $definitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
377             if (count($definitions) == 0) {
378                 throw new Tinebase_Exception_NotFound('Export definition for model ' .
379                     $this->_modelName . ' not found.');
380             }
381             $definition = $definitions->getFirstRecord();
382         }
383
384         $config = Tinebase_ImportExportDefinition::getInstance()->
385             getOptionsAsZendConfigXml($definition, $_additionalOptions);
386
387         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
388             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' export config: ' .
389                 print_r($config->toArray(), true));
390         }
391
392         return $config;
393     }
394
395     protected function _getTemplateFilename()
396     {
397         return $this->_templateFileName;
398     }
399
400     /**
401      * get export format string (csv, ...)
402      *
403      * @return string
404      * @throws Tinebase_Exception_NotFound
405      */
406     public function getFormat()
407     {
408         if ($this->_format === null) {
409             throw new Tinebase_Exception_NotFound('Format string not found.');
410         }
411
412         return $this->_format;
413     }
414
415     public static function getDefaultFormat()
416     {
417         return null;
418     }
419
420     /**
421      * get download content type
422      *
423      * @return string
424      */
425     abstract public function getDownloadContentType();
426
427     /**
428      * return download filename
429      *
430      * @param string $_appName
431      * @param string $_format
432      * @return string
433      */
434     public function getDownloadFilename($_appName, $_format)
435     {
436         return 'export_' . strtolower($_appName) . '.' . $_format;
437     }
438
439
440     /**
441      * workflow
442      * generate();
443      * * _exportRecords();
444      * * * if _hasTwig()
445      * * * * _loadTwig();
446      * * * * * _getTwigSource();
447      * * processIteration();
448      * * * _resolveRecords();
449      * * * if _firstIteration && _writeGenericHeader
450      * * * * _writeGenericHead();
451      * * * foreach $records
452      * * * * _startRow();
453      * * * * _processRecord();
454      * * * * _endRow();
455      * * _onAfterExportRecords();
456      */
457     /**
458      * generate export
459      */
460     abstract public function generate();
461
462
463     /**
464      * export records
465      */
466     protected function _exportRecords()
467     {
468         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
469             . ' Starting export of ' . $this->_modelName . ' with filter: ' . print_r($this->_filter->toArray(), true)
470             . ' and sort info: ' . print_r($this->_sortInfo, true));
471
472         if (true === $this->_hasTwig()) {
473             $this->_loadTwig();
474         }
475
476         $this->_onBeforeExportRecords();
477
478         $this->_firstIteration = true;
479
480         if (null === $this->_records) {
481             $iterator = new Tinebase_Record_Iterator(array(
482                 'iteratable' => $this,
483                 'controller' => $this->_controller,
484                 'filter' => $this->_filter,
485                 'options' => array(
486                     'searchAction' => 'export',
487                     'sortInfo' => $this->_sortInfo,
488                     'getRelations' => $this->_getRelations,
489                 ),
490             ));
491
492             if (false === ($result = $iterator->iterate())) {
493                 $result = array(
494                     'totalcount' => $this->_records->count(),
495                     'results'    => array(),
496                 );
497             }
498         } else {
499             $totalCount = 0;
500             $totalCountFn = function(&$val) use (&$totalCountFn, &$totalCount) {
501                 if (is_array($val)) {
502                     foreach ($val as &$a) {
503                         $totalCountFn($a);
504                     }
505                 } else {
506                     /** @var Tinebase_Record_RecordSet $val */
507                     $totalCount += $val->count();
508                 }
509             };
510             $totalCountFn($this->_records);
511
512             $result = array(
513                 'totalcount' => $totalCount,
514                 'results'    => array(),
515             );
516             $result['results'][] = $this->processIteration($this->_records);
517         }
518
519         $this->_onAfterExportRecords($result);
520
521         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
522             . ' Exported ' . $result['totalcount'] . ' records.');
523     }
524
525     protected function _onBeforeExportRecords()
526     {
527     }
528
529     /**
530      * @return bool
531      */
532     protected function _hasTwig()
533     {
534         if (true === $this->_hasTemplate) {
535             return true;
536         }
537         if ($this->_config->columns && $this->_config->columns->column) {
538             foreach ($this->_config->columns->column as $column) {
539                 if ($column->twig) {
540                     return true;
541                 }
542             }
543         }
544         return false;
545     }
546
547     protected function _loadTwig()
548     {
549         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' loading twig template...');
550
551         $tineTwigLoader = new Twig_Loader_Chain(array(
552             new Tinebase_Twig_CallBackLoader($this->_templateFileName, $this->_getLastModifiedTimeStamp(),
553                 array($this, '_getTwigSource'))));
554
555         // TODO turn on caching
556         // in order to cache the templates, we need to cache $this->_twigMapping too!
557         /*
558         $cacheDir = rtrim(Tinebase_Core::getTempDir(), '/') . '/tine20Twig';
559         if (!is_dir($cacheDir)) {
560             mkdir($cacheDir, 0777, true);
561         }*/
562
563         $this->_twigEnvironment = new Twig_Environment($tineTwigLoader, array(
564             'autoescape' => 'json',
565             'cache' => false, //$cacheDir
566         ));
567         /** @noinspection PhpUndefinedMethodInspection */
568         /** @noinspection PhpUnusedParameterInspection */
569         $this->_twigEnvironment->getExtension('core')->setEscaper('json', function($twigEnv, $string, $charset) {
570             return json_encode($string);
571         });
572
573         $locale = $this->_locale;
574         $translate = $this->_translate;
575         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('translate',
576             function ($str) use($locale, $translate) {
577                 return $translate->_($str, $locale);
578             }));
579         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('dateFormat', function ($date, $format) {
580             return Tinebase_Translation::dateToStringInTzAndLocaleFormat($date, null, null, $format);
581         }));
582         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('relationTranslateModel', function ($model) {
583             // TODO implement this!
584             return $model;
585         }));
586
587         $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName);
588     }
589
590     /**
591      * @return string
592      */
593     public function _getTwigSource()
594     {
595         $source = '[';
596         if (true !== $this->_hasTemplate && $this->_config->columns && $this->_config->columns->column) {
597             foreach ($this->_config->columns->column as $column) {
598                 if ($column->twig) {
599                     $source .= ($source!=='' ? ',"' : '""') . (string)$column->twig . '"';
600                 }
601             }
602         }
603         return $source . ']';
604     }
605
606     /**
607      * @return int
608      */
609     protected function _getLastModifiedTimeStamp()
610     {
611         return filemtime($this->_templateFileName);
612     }
613
614     protected function _getCurrentState()
615     {
616         return array(
617             '_firstIteration'       => $this->_firstIteration,
618             '_writeGenericHeader'   => $this->_writeGenericHeader,
619             '_groupByProperty'      => $this->_groupByProperty,
620             '_groupByProcessor'     => $this->_groupByProcessor,
621             '_lastGroupValue'       => $this->_lastGroupValue,
622             '_currentRecord'        => $this->_currentRecord,
623             '_currentRowType'       => $this->_currentRowType,
624             '_twigTemplate'         => $this->_twigTemplate,
625             '_twigMapping'          => $this->_twigMapping,
626         );
627     }
628
629     protected function _setCurrentState(array $array)
630     {
631         foreach ($array as $key => $value) {
632             $this->{$key} = $value;
633         }
634     }
635
636     /**
637      * add body rows
638      *
639      * @param Tinebase_Record_RecordSet|array $_records
640      */
641     public function processIteration($_records)
642     {
643         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' iterating over export data...');
644
645         if (is_array($_records)) {
646
647             foreach ($_records as $key => $value) {
648                 $this->_startDataSource($key);
649
650                 $this->processIteration($value);
651
652                 $this->_endDataSource($key);
653             }
654
655             return;
656         }
657
658         $this->_resolveRecords($_records);
659
660         if (true === $this->_firstIteration && true === $this->_writeGenericHeader) {
661             $this->_writeGenericHead();
662         }
663
664         $first = $this->_firstIteration;
665         foreach ($_records as $record) {
666             if (null !== $this->_groupByProperty) {
667                 $propertyValue = $record->{$this->_groupByProperty};
668                 if (null !== $this->_groupByProcessor) {
669                     /** @var closure $fn */
670                     $fn = $this->_groupByProcessor;
671                     $fn($propertyValue);
672                 }
673                 if (true === $first || $this->_lastGroupValue !== $propertyValue) {
674                     $this->_lastGroupValue = $propertyValue;
675                     if (false === $first) {
676                         $this->_endGroup();
677                     }
678                     $this->_currentRecord = $record;
679                     $this->_startGroup();
680                 }
681                 // TODO fix this?
682                 //$this->_writeGroupHeading($record);
683             }
684             $this->_currentRecord = $record;
685
686             $this->_currentRowType = self::ROW_TYPE_RECORD;
687
688             $this->_startRow();
689
690             $this->_processRecord($record);
691
692             $this->_endRow();
693
694             if (true === $first) {
695                 $first = false;
696             }
697         }
698
699         if ($_records->count() > 0 && null !== $this->_groupByProperty) {
700             $this->_endGroup();
701         }
702
703         $this->_firstIteration = false;
704     }
705
706     /**
707      * @param $_name
708      */
709     protected function _startDataSource($_name)
710     {
711     }
712
713     /**
714      * @param $_name
715      */
716     protected function _endDataSource($_name)
717     {
718     }
719
720     protected function _startGroup()
721     {
722     }
723
724     protected function _endGroup()
725     {
726     }
727
728     protected function _writeGroupHeading(Tinebase_Record_Interface $_record)
729     {
730         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writting group heading...');
731
732         $this->_currentRowType = self::ROW_TYPE_GROUP_HEADER;
733
734         $this->_startRow();
735
736         $this->_writeValue($_record->{$this->_groupByProperty});
737
738         $this->_endRow();
739     }
740
741     /**
742      * resolve records and prepare for export (set user timezone, ...)
743      *
744      * @param Tinebase_Record_RecordSet $_records
745      */
746     protected function _resolveRecords(Tinebase_Record_RecordSet $_records)
747     {
748         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' resolving export records...');
749         if ($_records->count() === 0) {
750             return;
751         }
752         $record = $_records->getFirstRecord();
753         // FIXME think what to do
754         // TODO fix ALL this!
755
756         // get field types/identifiers from config
757         $identifiers = array();
758         if ($this->_config->columns) {
759             $types = array();
760             foreach ($this->_config->columns->column as $column) {
761                 $types[] = $column->type;
762                 $identifiers[] = $column->identifier;
763             }
764             $types = array_unique($types);
765         } else {
766             $types = $this->_resolvedFields;
767         }
768
769         // resolve users
770         foreach ($this->_userFields as $field) {
771             if (in_array($field, $types) || in_array($field, $identifiers)) {
772                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resolving users for ' . $field);
773                 Tinebase_User::getInstance()->resolveMultipleUsers($_records, $field, true);
774             }
775         }
776
777         // add notes
778         if (in_array('notes', $types)) {
779             Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($_records, 'notes', 'Sql', false);
780         }
781
782         // add container
783         if (in_array('container_id', $types)) {
784             Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
785         }
786
787         if ($record->has('customfields')) {
788             $_records->customfields = array();
789             Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($_records, true);
790         }
791
792         if ($record->has('relations')) {
793             /** @var Tinebase_Record_Abstract $modelName */
794             $modelName = $_records->getRecordClassName();
795
796             $relations = Tinebase_Relations::getInstance()->getMultipleRelations($modelName, 'Sql',
797                 $_records->getArrayOfIds());
798
799             $appConfig = Tinebase_Config::factory($this->_applicationName);
800             $modelConfig = $modelName::getConfiguration();
801
802             /** @var Tinebase_Record_Abstract $record */
803             foreach ($_records as $idx => $record) {
804                 if (isset($relations[$idx])) {
805                     $record->relations = $relations[$idx];
806                 }
807
808                 if (null === $modelConfig) {
809                     foreach ($this->_keyFields as $name => $keyField) {
810                         /** @var Tinebase_Config_KeyField $keyField */
811                         $keyField = $appConfig->{$keyField};
812                         $record->{$name} = $keyField->getTranslatedValue($record->{$name});
813                     }
814
815                     foreach ($this->_virtualFields as $name => $virtualField) {
816                         $value = null;
817                         if (!empty($record->relations)) {
818                             /** @var Tinebase_Model_Relation $relation */
819                             foreach ($record->relations as $relation) {
820                                 if ($relation->related_model === $virtualField['relatedModel'] &&
821                                     $relation->related_degree === $virtualField['relatedDegree'] &&
822                                     $relation->type === $virtualField['type']
823                                 ) {
824                                     $value = $relation->related_record;
825                                     break;
826                                 }
827                             }
828                         }
829                         $record->{$name} = $value;
830                     }
831
832                     foreach ($this->_foreignIdFields as $name => $controller) {
833                         if (!empty($record->{$name})) {
834                             /** @var Tinebase_Controller_Record_Abstract $controller */
835                             $controller = $controller::getInstance();
836                             $record->{$name} = $controller->get($record->{$name});
837                         }
838                     }
839                 } else {
840                     foreach ($modelConfig->virtualFields as $field) {
841                         // resolve virtual relation record from relations property
842                         if (isset($field['type']) && $field['type'] === 'relation') {
843                             $fc = $field['config'];
844                             if (!empty($record->relations)) {
845                                 foreach ($record->relations as $relation) {
846                                     if ($relation->type === $fc['type'] &&
847                                             $relation->related_model === ($fc['appName'] . '_Model_' . $fc['modelName'])) {
848                                         $record->{$field['key']} = $relation->related_record;
849                                     }
850                                 }
851                             }
852                         }
853                     }
854                 }
855             }
856         }
857
858         $_records->setTimezone(Tinebase_Core::getUserTimezone());
859     }
860
861     protected function _writeGenericHead()
862     {
863         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writing generic header...');
864
865         $this->_currentRowType = self::ROW_TYPE_GENERIC_HEADER;
866
867         $this->_startRow();
868
869         if ($this->_config->columns && $this->_config->columns->column) {
870             foreach ($this->_config->columns->column as $column) {
871                 if ($column->header) {
872                     $this->_writeValue($column->header);
873                 } elseif ($column->recordProperty) {
874                     $this->_writeValue($column->recordProperty);
875                 } else {
876                     $this->_writeValue('');
877                 }
878             }
879         } else {
880             /** @var Tinebase_Record_Abstract $record */
881             $record = new $this->_modelName(array(), true);
882
883             foreach ($record->getFields() as $field) {
884                 // TODO translate?
885                 $this->_writeValue($field);
886             }
887         }
888
889         $this->_endRow();
890     }
891
892     protected function _startRow()
893     {
894     }
895
896     /**
897      * @param Tinebase_Record_Interface $_record
898      */
899     protected function _processRecord(Tinebase_Record_Interface $_record)
900     {
901         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' processing a export record...');
902
903         if (true === $this->_dumpRecords) {
904             foreach ($_record->getFields() as $field) {
905                 $this->_writeValue($this->_convertToString($_record->{$field}));
906             }
907         } elseif (true !== $this->_hasTemplate) {
908             $twigResult = array();
909             if (null !== $this->_twigTemplate) {
910                 $result = json_decode($this->_twigTemplate->render(
911                     $this->_getTwigContext(array('record' => $_record))));
912                 if (is_array($result)) {
913                     $twigResult = $result;
914                 } else {
915                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
916                         ' twig render and json_decode did not return an array: ' . print_r($result, true));
917                 }
918             }
919             $twigCounter = 0;
920             foreach ($this->_config->columns->column as $column) {
921                 if ($column->twig) {
922                     if (isset($twigResult[$twigCounter]) || array_key_exists($twigCounter, $twigResult)) {
923                         $this->_writeValue($this->_convertToString($twigResult[$twigCounter]));
924                     } else {
925                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
926                             ' twig column: ' . $column->twig . ' not found in twig result array');
927                         $this->_writeValue('');
928                     }
929                 } elseif ($column->recordProperty) {
930                     $this->_writeValue($this->_convertToString($_record->{$column->recordProperty}));
931                 } else {
932                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
933                         ' pointless column found: ' . print_r($column, true));
934                 }
935             }
936         } elseif (null !== $this->_twigTemplate) {
937             $this->_renderTwigTemplate($_record);
938         } else {
939             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' can not process record, misconfigured!');
940         }
941     }
942
943     /**
944      * @param Tinebase_Record_Interface|null $_record
945      */
946     protected function _renderTwigTemplate($_record = null)
947     {
948         $twigResult = $this->_twigTemplate->render(
949             $this->_getTwigContext(array('record' => $_record)));
950         $twigResult = json_decode($twigResult);
951         if (!is_array($twigResult)) {
952             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
953                 ' twig render and json_decode did not return an array: ' . print_r($twigResult, true));
954             return;
955         }
956
957         foreach ($this->_twigMapping as $key => $twigKey) {
958             if (isset($twigResult[$key]) || array_key_exists($key, $twigResult)) {
959                 $value = $this->_convertToString($twigResult[$key]);
960             } else {
961                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
962                     ' twig mapping: ' . $key . ' ' . $twigKey . ' not found in twig result array');
963                 $value = '';
964             }
965             $this->_setValue($twigKey, $value);
966         }
967     }
968
969     /**
970      * @param array $context
971      * @return array
972      */
973     protected function _getTwigContext(array $context)
974     {
975
976         if (null === $this->_logoPath) {
977             $this->_logoPath = Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_LOGO};
978
979             if (strpos($this->_logoPath, '://') === false) {
980                 if ('.' === $this->_logoPath[0] && '/' === $this->_logoPath[1]) {
981                     $this->_logoPath = mb_substr($this->_logoPath, 1);
982                 } elseif ('/' !== $this->_logoPath[0]) {
983                     $this->_logoPath = '/' . $this->_logoPath;
984                 }
985
986                 $baseDir = dirname(dirname(__DIR__));
987                 if (0 === strpos($this->_logoPath, $baseDir)) {
988                     $this->_logoPath = 'file://' . $this->_logoPath;
989                 } else {
990                     $this->_logoPath = 'file://' . $baseDir . $this->_logoPath;
991                 }
992
993                 if (!is_file($this->_logoPath)) {
994                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' can not find branding logo. Config: ' . Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_LOGO} . ' path: ' . $this->_logoPath);
995                     $this->_logoPath = false;
996                 }
997             }
998         }
999
1000
1001         return array_merge(array(
1002             'branding'          => array(
1003                 'logo'              => $this->_logoPath,
1004                 'title'             => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_TITLE},
1005                 'description'       => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_DESCRIPTION},
1006                 'weburl'            => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_WEBURL},
1007             ),
1008             'export'            => array(
1009                 'timestamp'         => $this->_exportTimeStamp,
1010                 'account'           => Tinebase_Core::getUser(),
1011                 'contact'           => Addressbook_Controller_Contact::getInstance()->get(Tinebase_Core::getUser()
1012                                         ->contact_id),
1013                 'groupdata'         => $this->_lastGroupValue,
1014             ),
1015             'additionalRecords' => $this->_additionalRecords,
1016         ), $context);
1017     }
1018
1019     /**
1020      * @param string $_key
1021      * @param string $_value
1022      */
1023     abstract protected function _setValue($_key, $_value);
1024
1025     /**
1026      * @param string $_value
1027      */
1028     abstract protected function _writeValue($_value);
1029
1030     /**
1031      * @param mixed $_value
1032      * @return string
1033      */
1034     protected function _convertToString($_value)
1035     {
1036         if (is_null($_value)) {
1037             $_value = '';
1038         }
1039
1040         if ($_value instanceof DateTime) {
1041             $_value = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_value, null, null,
1042                 $this->_config->datetimeformat);
1043         }
1044
1045         if (is_object($_value) && method_exists($_value, '__toString')) {
1046             $_value = $_value->__toString();
1047         }
1048
1049         if (!is_scalar($_value)) {
1050             $_value = '';
1051         }
1052
1053         return (string)$_value;
1054     }
1055
1056     protected function _endRow()
1057     {
1058     }
1059
1060     /**
1061      * set generic data
1062      *
1063      * @param array $result
1064      */
1065     protected function _onAfterExportRecords(/** @noinspection PhpUnusedParameterInspection */ array $result)
1066     {
1067         $this->_iterationDone = true;
1068
1069         if (null !== $this->_twigTemplate) {
1070             $this->_renderTwigTemplate();
1071         }
1072     }
1073 }