a867147fb02fe84bf3725d0716d071b733491b8f
[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 (isset($_additionalOptions['template'])) {
248             try {
249                 $path = Tinebase_Model_Tree_Node_Path::createFromStatPath(Tinebase_FileSystem::getInstance()->getPathOfNode($_additionalOptions['template'], true));
250                 $this->_templateFileName = $path->streamwrapperpath;
251             } catch (Exception $e) {}
252         }
253         if (! $this->_modelName && !empty($this->_config->model)) {
254             $this->_modelName = $this->_config->model;
255         }
256         $this->_exportTimeStamp = Tinebase_DateTime::now();
257
258         if (!empty($this->_config->group)) {
259             $this->_groupByProperty = $this->_config->group;
260             $this->_sortInfo['sort'] = $this->_groupByProperty;
261             if (!empty($this->_config->groupSortDir)) {
262                 $this->_sortInfo['dir'] = $this->_config->groupSortDir;
263             }
264         }
265
266         if (isset($_additionalOptions['sortInfo'])) {
267             if (isset($this->_sortInfo['sort'])) {
268                 $this->_sortInfo['sort'] = array_unique(array_merge((array)$this->_sortInfo['sort'],
269                     (array)((isset($_additionalOptions['sortInfo']['field']) ?
270                         $_additionalOptions['sortInfo']['field'] : $_additionalOptions['sortInfo']['sort']))));
271             } else {
272                 if (isset($_additionalOptions['sortInfo']['field'])) {
273                     $this->_sortInfo['sort'] = $_additionalOptions['sortInfo']['field'];
274                     $this->_sortInfo['dir'] = isset($_additionalOptions['sortInfo']['direction']) ?
275                         $_additionalOptions['sortInfo']['direction'] : 'ASC';
276                 } else {
277                     $this->_sortInfo = $_additionalOptions['sortInfo'];
278                 }
279             }
280         }
281
282         if (isset($_additionalOptions['recordData'])) {
283             if (isset($_additionalOptions['recordData']['container_id']) && is_array($_additionalOptions['recordData']['container_id'])) {
284                 $_additionalOptions['recordData']['container_id'] = $_additionalOptions['recordData']['container_id']['id'];
285             }
286             $this->_records = new Tinebase_Record_RecordSet($this->_modelName,
287                 array(new $this->_modelName($_additionalOptions['recordData'])));
288         }
289
290         if (isset($_additionalOptions['additionalRecords'])) {
291             foreach ($_additionalOptions['additionalRecords'] as $key => $value) {
292                 if (!isset($value['model']) || !isset($value['recordData'])) {
293                     throw new Tinebase_Exception_UnexpectedValue('additionalRecords needs to specify model and recordData');
294                 }
295                 $record = new $value['model']($value['recordData']);
296                 $this->_additionalRecords[$key] = $record;
297             }
298         }
299
300         if ($this->_config->keyFields) {
301             foreach ($this->_config->keyFields as $keyFields) {
302                 if ($keyFields->propertyName) {
303                     $keyFields = array($keyFields);
304                 }
305                 foreach($keyFields as $keyField) {
306                     $this->_keyFields[$keyField->propertyName] = $keyField->name;
307                 }
308             }
309         }
310
311         if ($this->_config->foreignIds) {
312             foreach ($this->_config->foreignIds as $foreignIds) {
313                 if ($foreignIds->controller) {
314                     $foreignIds = array($foreignIds);
315                 }
316                 foreach($foreignIds as $foreignId) {
317                     $this->_foreignIdFields[$foreignId->name] = $foreignId->controller;
318                 }
319             }
320         }
321
322         if ($this->_config->virtualFields) {
323             foreach ($this->_config->virtualFields as $virtualFields) {
324                 if ($virtualFields->relatedModel) {
325                     $virtualFields = array($virtualFields);
326                 }
327                 foreach($virtualFields as $virtualField) {
328                     $this->_virtualFields[$virtualField->name] = array(
329                         'relatedModel' => $virtualField->relatedModel,
330                         'relatedDegree' => $virtualField->relatedDegree,
331                         'type' => $virtualField->type
332                     );
333                 }
334             }
335         }
336     }
337
338     /**
339      * get export config
340      *
341      * @param array $_additionalOptions additional options
342      * @return Zend_Config_Xml
343      * @throws Tinebase_Exception_NotFound
344      */
345     protected function _getExportConfig($_additionalOptions = array())
346     {
347         if ((isset($_additionalOptions['definitionFilename']) ||
348             array_key_exists('definitionFilename', $_additionalOptions))) {
349             // get definition from file
350             $definition = Tinebase_ImportExportDefinition::getInstance()->getFromFile(
351                 $_additionalOptions['definitionFilename'],
352                 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)->getId()
353             );
354         } elseif ((isset($_additionalOptions['definitionId']) ||
355             array_key_exists('definitionId', $_additionalOptions))) {
356             $definition = Tinebase_ImportExportDefinition::getInstance()->get($_additionalOptions['definitionId']);
357         } else {
358             // get preference from db and set export definition name
359             $exportName = $this->_defaultExportname;
360             if ($this->_prefKey !== null) {
361                 $exportName = Tinebase_Core::getPreference($this->_applicationName)->
362                     getValue($this->_prefKey, $exportName);
363             }
364
365             // get export definition by name / model
366             $filter = new Tinebase_Model_ImportExportDefinitionFilter(array(
367                 array('field' => 'model', 'operator' => 'equals', 'value' => $this->_modelName),
368                 array('field' => 'name',  'operator' => 'equals', 'value' => $exportName),
369             ));
370             $definitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
371             if (count($definitions) == 0) {
372                 throw new Tinebase_Exception_NotFound('Export definition for model ' .
373                     $this->_modelName . ' not found.');
374             }
375             $definition = $definitions->getFirstRecord();
376         }
377
378         $config = Tinebase_ImportExportDefinition::getInstance()->
379             getOptionsAsZendConfigXml($definition, $_additionalOptions);
380
381         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
382             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' export config: ' .
383                 print_r($config->toArray(), true));
384         }
385
386         return $config;
387     }
388
389     protected function _getTemplateFilename()
390     {
391         return $this->_templateFileName;
392     }
393
394     /**
395      * get export format string (csv, ...)
396      *
397      * @return string
398      * @throws Tinebase_Exception_NotFound
399      */
400     public function getFormat()
401     {
402         if ($this->_format === null) {
403             throw new Tinebase_Exception_NotFound('Format string not found.');
404         }
405
406         return $this->_format;
407     }
408
409     public static function getDefaultFormat()
410     {
411         return null;
412     }
413
414     /**
415      * get download content type
416      *
417      * @return string
418      */
419     abstract public function getDownloadContentType();
420
421     /**
422      * return download filename
423      *
424      * @param string $_appName
425      * @param string $_format
426      * @return string
427      */
428     public function getDownloadFilename($_appName, $_format)
429     {
430         return 'export_' . strtolower($_appName) . '.' . $_format;
431     }
432
433
434     /**
435      * workflow
436      * generate();
437      * * _exportRecords();
438      * * * if _hasTwig()
439      * * * * _loadTwig();
440      * * * * * _getTwigSource();
441      * * processIteration();
442      * * * _resolveRecords();
443      * * * if _firstIteration && _writeGenericHeader
444      * * * * _writeGenericHead();
445      * * * foreach $records
446      * * * * _startRow();
447      * * * * _processRecord();
448      * * * * _endRow();
449      * * _onAfterExportRecords();
450      */
451     /**
452      * generate export
453      */
454     abstract public function generate();
455
456
457     /**
458      * export records
459      */
460     protected function _exportRecords()
461     {
462         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
463             . ' Starting export of ' . $this->_modelName . ' with filter: ' . print_r($this->_filter->toArray(), true)
464             . ' and sort info: ' . print_r($this->_sortInfo, true));
465
466         if (true === $this->_hasTwig()) {
467             $this->_loadTwig();
468         }
469
470         $this->_onBeforeExportRecords();
471
472         $this->_firstIteration = true;
473
474         if (null === $this->_records) {
475             $iterator = new Tinebase_Record_Iterator(array(
476                 'iteratable' => $this,
477                 'controller' => $this->_controller,
478                 'filter' => $this->_filter,
479                 'options' => array(
480                     'searchAction' => 'export',
481                     'sortInfo' => $this->_sortInfo,
482                     'getRelations' => $this->_getRelations,
483                 ),
484             ));
485
486             if (false === ($result = $iterator->iterate())) {
487                 $result = array(
488                     'totalcount' => $this->_records->count(),
489                     'results'    => array(),
490                 );
491             }
492         } else {
493             $totalCount = 0;
494             $totalCountFn = function(&$val) use (&$totalCountFn, &$totalCount) {
495                 if (is_array($val)) {
496                     foreach ($val as &$a) {
497                         $totalCountFn($a);
498                     }
499                 } else {
500                     /** @var Tinebase_Record_RecordSet $val */
501                     $totalCount += $val->count();
502                 }
503             };
504             $totalCountFn($this->_records);
505
506             $result = array(
507                 'totalcount' => $totalCount,
508                 'results'    => array(),
509             );
510             $result['results'][] = $this->processIteration($this->_records);
511         }
512
513         $this->_onAfterExportRecords($result);
514
515         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
516             . ' Exported ' . $result['totalcount'] . ' records.');
517     }
518
519     protected function _onBeforeExportRecords()
520     {
521     }
522
523     /**
524      * @return bool
525      */
526     protected function _hasTwig()
527     {
528         if (true === $this->_hasTemplate) {
529             return true;
530         }
531         if ($this->_config->columns && $this->_config->columns->column) {
532             foreach ($this->_config->columns->column as $column) {
533                 if ($column->twig) {
534                     return true;
535                 }
536             }
537         }
538         return false;
539     }
540
541     protected function _loadTwig()
542     {
543         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' loading twig template...');
544
545         $tineTwigLoader = new Twig_Loader_Chain(array(
546             new Tinebase_Twig_CallBackLoader($this->_templateFileName, $this->_getLastModifiedTimeStamp(),
547                 array($this, '_getTwigSource'))));
548
549         // TODO turn on caching
550         // in order to cache the templates, we need to cache $this->_twigMapping too!
551         /*
552         $cacheDir = rtrim(Tinebase_Core::getTempDir(), '/') . '/tine20Twig';
553         if (!is_dir($cacheDir)) {
554             mkdir($cacheDir, 0777, true);
555         }*/
556
557         $this->_twigEnvironment = new Twig_Environment($tineTwigLoader, array(
558             'autoescape' => 'json',
559             'cache' => false, //$cacheDir
560         ));
561         /** @noinspection PhpUndefinedMethodInspection */
562         /** @noinspection PhpUnusedParameterInspection */
563         $this->_twigEnvironment->getExtension('core')->setEscaper('json', function($twigEnv, $string, $charset) {
564             return json_encode($string);
565         });
566
567         $locale = $this->_locale;
568         $translate = $this->_translate;
569         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('translate',
570             function ($str) use($locale, $translate) {
571                 return $translate->_($str, $locale);
572             }));
573         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('dateFormat', function ($date, $format) {
574             return Tinebase_Translation::dateToStringInTzAndLocaleFormat($date, null, null, $format);
575         }));
576
577         $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName);
578     }
579
580     /**
581      * @return string
582      */
583     public function _getTwigSource()
584     {
585         $source = '[';
586         if (true !== $this->_hasTemplate && $this->_config->columns && $this->_config->columns->column) {
587             foreach ($this->_config->columns->column as $column) {
588                 if ($column->twig) {
589                     $source .= ($source!=='' ? ',"' : '""') . (string)$column->twig . '"';
590                 }
591             }
592         }
593         return $source . ']';
594     }
595
596     /**
597      * @return int
598      */
599     protected function _getLastModifiedTimeStamp()
600     {
601         return filemtime($this->_templateFileName);
602     }
603
604     protected function _getCurrentState()
605     {
606         return array(
607             '_firstIteration'       => $this->_firstIteration,
608             '_writeGenericHeader'   => $this->_writeGenericHeader,
609             '_groupByProperty'      => $this->_groupByProperty,
610             '_groupByProcessor'     => $this->_groupByProcessor,
611             '_lastGroupValue'       => $this->_lastGroupValue,
612             '_currentRecord'        => $this->_currentRecord,
613             '_currentRowType'       => $this->_currentRowType,
614             '_twigTemplate'         => $this->_twigTemplate,
615             '_twigMapping'          => $this->_twigMapping,
616         );
617     }
618
619     protected function _setCurrentState(array $array)
620     {
621         foreach ($array as $key => $value) {
622             $this->{$key} = $value;
623         }
624     }
625
626     /**
627      * add body rows
628      *
629      * @param Tinebase_Record_RecordSet|array $_records
630      */
631     public function processIteration($_records)
632     {
633         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' iterating over export data...');
634
635         if (is_array($_records)) {
636
637             foreach ($_records as $key => $value) {
638                 $this->_startDataSource($key);
639
640                 $this->processIteration($value);
641
642                 $this->_endDataSource($key);
643             }
644
645             return;
646         }
647
648         $this->_resolveRecords($_records);
649
650         if (true === $this->_firstIteration && true === $this->_writeGenericHeader) {
651             $this->_writeGenericHead();
652         }
653
654         $first = $this->_firstIteration;
655         foreach ($_records as $record) {
656             if (null !== $this->_groupByProperty) {
657                 $propertyValue = $record->{$this->_groupByProperty};
658                 if (null !== $this->_groupByProcessor) {
659                     /** @var closure $fn */
660                     $fn = $this->_groupByProcessor;
661                     $fn($propertyValue);
662                 }
663                 if (true === $first || $this->_lastGroupValue !== $propertyValue) {
664                     $this->_lastGroupValue = $propertyValue;
665                     if (false === $first) {
666                         $this->_endGroup();
667                     }
668                     $this->_currentRecord = $record;
669                     $this->_startGroup();
670                 }
671                 // TODO fix this?
672                 //$this->_writeGroupHeading($record);
673             }
674             $this->_currentRecord = $record;
675
676             $this->_currentRowType = self::ROW_TYPE_RECORD;
677
678             $this->_startRow();
679
680             $this->_processRecord($record);
681
682             $this->_endRow();
683
684             if (true === $first) {
685                 $first = false;
686             }
687         }
688
689         if ($_records->count() > 0 && null !== $this->_groupByProperty) {
690             $this->_endGroup();
691         }
692
693         $this->_firstIteration = false;
694     }
695
696     /**
697      * @param $_name
698      */
699     protected function _startDataSource($_name)
700     {
701     }
702
703     /**
704      * @param $_name
705      */
706     protected function _endDataSource($_name)
707     {
708     }
709
710     protected function _startGroup()
711     {
712     }
713
714     protected function _endGroup()
715     {
716     }
717
718     protected function _writeGroupHeading(Tinebase_Record_Interface $_record)
719     {
720         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writting group heading...');
721
722         $this->_currentRowType = self::ROW_TYPE_GROUP_HEADER;
723
724         $this->_startRow();
725
726         $this->_writeValue($_record->{$this->_groupByProperty});
727
728         $this->_endRow();
729     }
730
731     /**
732      * resolve records and prepare for export (set user timezone, ...)
733      *
734      * @param Tinebase_Record_RecordSet $_records
735      */
736     protected function _resolveRecords(Tinebase_Record_RecordSet $_records)
737     {
738         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' resolving export records...');
739         if ($_records->count() === 0) {
740             return;
741         }
742         $record = $_records->getFirstRecord();
743         // FIXME think what to do
744         // TODO fix ALL this!
745
746         // get field types/identifiers from config
747         $identifiers = array();
748         if ($this->_config->columns) {
749             $types = array();
750             foreach ($this->_config->columns->column as $column) {
751                 $types[] = $column->type;
752                 $identifiers[] = $column->identifier;
753             }
754             $types = array_unique($types);
755         } else {
756             $types = $this->_resolvedFields;
757         }
758
759         // resolve users
760         foreach ($this->_userFields as $field) {
761             if (in_array($field, $types) || in_array($field, $identifiers)) {
762                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resolving users for ' . $field);
763                 Tinebase_User::getInstance()->resolveMultipleUsers($_records, $field, true);
764             }
765         }
766
767         // add notes
768         if (in_array('notes', $types)) {
769             Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($_records, 'notes', 'Sql', false);
770         }
771
772         // add container
773         if (in_array('container_id', $types)) {
774             Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
775         }
776
777         if ($record->has('customfields')) {
778             $_records->customfields = array();
779             Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($_records, true);
780         }
781
782         if ($record->has('relations')) {
783             /** @var Tinebase_Record_Abstract $modelName */
784             $modelName = $_records->getRecordClassName();
785
786             $relations = Tinebase_Relations::getInstance()->getMultipleRelations($modelName, 'Sql',
787                 $_records->getArrayOfIds());
788
789             $appConfig = Tinebase_Config::factory($this->_applicationName);
790             $modelConfig = $modelName::getConfiguration();
791
792             /** @var Tinebase_Record_Abstract $record */
793             foreach ($_records as $idx => $record) {
794                 if (isset($relations[$idx])) {
795                     $record->relations = $relations[$idx];
796                 }
797
798                 if (null === $modelConfig) {
799                     foreach ($this->_keyFields as $name => $keyField) {
800                         /** @var Tinebase_Config_KeyField $keyField */
801                         $keyField = $appConfig->{$keyField};
802                         $record->{$name} = $keyField->getTranslatedValue($record->{$name});
803                     }
804
805                     foreach ($this->_virtualFields as $name => $virtualField) {
806                         $value = null;
807                         if (!empty($record->relations)) {
808                             /** @var Tinebase_Model_Relation $relation */
809                             foreach ($record->relations as $relation) {
810                                 if ($relation->related_model === $virtualField['relatedModel'] &&
811                                     $relation->related_degree === $virtualField['relatedDegree'] &&
812                                     $relation->type === $virtualField['type']
813                                 ) {
814                                     $value = $relation->related_record;
815                                     break;
816                                 }
817                             }
818                         }
819                         $record->{$name} = $value;
820                     }
821
822                     foreach ($this->_foreignIdFields as $name => $controller) {
823                         if (!empty($record->{$name})) {
824                             $controller = $controller::getInstance();
825                             $record->{$name} = $controller->get($record->{$name});
826                         }
827                     }
828                 } else {
829                     foreach ($modelConfig->virtualFields as $field) {
830                         // resolve virtual relation record from relations property
831                         if (isset($field['type']) && $field['type'] === 'relation') {
832                             $fc = $field['config'];
833                             if (!empty($record->relations)) {
834                                 foreach ($record->relations as $relation) {
835                                     if ($relation->type === $fc['type'] &&
836                                             $relation->related_model === ($fc['appName'] . '_Model_' . $fc['modelName'])) {
837                                         $record->{$field['key']} = $relation->related_record;
838                                     }
839                                 }
840                             }
841                         }
842                     }
843                 }
844             }
845         }
846
847         $_records->setTimezone(Tinebase_Core::getUserTimezone());
848     }
849
850     protected function _writeGenericHead()
851     {
852         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writing generic header...');
853
854         $this->_currentRowType = self::ROW_TYPE_GENERIC_HEADER;
855
856         $this->_startRow();
857
858         if ($this->_config->columns && $this->_config->columns->column) {
859             foreach ($this->_config->columns->column as $column) {
860                 if ($column->header) {
861                     $this->_writeValue($column->header);
862                 } elseif ($column->recordProperty) {
863                     $this->_writeValue($column->recordProperty);
864                 } else {
865                     $this->_writeValue('');
866                 }
867             }
868         } else {
869             /** @var Tinebase_Record_Abstract $record */
870             $record = new $this->_modelName(array(), true);
871
872             foreach ($record->getFields() as $field) {
873                 // TODO translate?
874                 $this->_writeValue($field);
875             }
876         }
877
878         $this->_endRow();
879     }
880
881     protected function _startRow()
882     {
883     }
884
885     /**
886      * @param Tinebase_Record_Interface $_record
887      */
888     protected function _processRecord(Tinebase_Record_Interface $_record)
889     {
890         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' processing a export record...');
891
892         if (true === $this->_dumpRecords) {
893             foreach ($_record->getFields() as $field) {
894                 $this->_writeValue($this->_convertToString($_record->{$field}));
895             }
896         } elseif (true !== $this->_hasTemplate) {
897             $twigResult = array();
898             if (null !== $this->_twigTemplate) {
899                 $result = json_decode($this->_twigTemplate->render(
900                     $this->_getTwigContext(array('record' => $_record))));
901                 if (is_array($result)) {
902                     $twigResult = $result;
903                 } else {
904                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
905                         ' twig render and json_decode did not return an array: ' . print_r($result, true));
906                 }
907             }
908             $twigCounter = 0;
909             foreach ($this->_config->columns->column as $column) {
910                 if ($column->twig) {
911                     if (isset($twigResult[$twigCounter]) || array_key_exists($twigCounter, $twigResult)) {
912                         $this->_writeValue($this->_convertToString($twigResult[$twigCounter]));
913                     } else {
914                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
915                             ' twig column: ' . $column->twig . ' not found in twig result array');
916                         $this->_writeValue('');
917                     }
918                 } elseif ($column->recordProperty) {
919                     $this->_writeValue($this->_convertToString($_record->{$column->recordProperty}));
920                 } else {
921                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
922                         ' pointless column found: ' . print_r($column, true));
923                 }
924             }
925         } elseif (null !== $this->_twigTemplate) {
926             $this->_renderTwigTemplate($_record);
927         } else {
928             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' can not process record, misconfigured!');
929         }
930     }
931
932     /**
933      * @param Tinebase_Record_Interface|null $_record
934      */
935     protected function _renderTwigTemplate($_record = null)
936     {
937         $twigResult = $this->_twigTemplate->render(
938             $this->_getTwigContext(array('record' => $_record)));
939         $twigResult = json_decode($twigResult);
940         if (!is_array($twigResult)) {
941             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
942                 ' twig render and json_decode did not return an array: ' . print_r($twigResult, true));
943             return;
944         }
945
946         foreach ($this->_twigMapping as $key => $twigKey) {
947             if (isset($twigResult[$key]) || array_key_exists($key, $twigResult)) {
948                 $value = $this->_convertToString($twigResult[$key]);
949             } else {
950                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
951                     ' twig mapping: ' . $key . ' ' . $twigKey . ' not found in twig result array');
952                 $value = '';
953             }
954             $this->_setValue($twigKey, $value);
955         }
956     }
957
958     /**
959      * @param array $context
960      * @return array
961      */
962     protected function _getTwigContext(array $context)
963     {
964
965         if (null === $this->_logoPath) {
966             $this->_logoPath = Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_LOGO};
967
968             if (strpos($this->_logoPath, '://') === false) {
969                 if ('.' === $this->_logoPath[0] && '/' === $this->_logoPath[1]) {
970                     $this->_logoPath = mb_substr($this->_logoPath, 1);
971                 } elseif ('/' !== $this->_logoPath[0]) {
972                     $this->_logoPath = '/' . $this->_logoPath;
973                 }
974
975                 $baseDir = dirname(dirname(__DIR__));
976                 if (0 === strpos($this->_logoPath, $baseDir)) {
977                     $this->_logoPath = 'file://' . $this->_logoPath;
978                 } else {
979                     $this->_logoPath = 'file://' . $baseDir . $this->_logoPath;
980                 }
981
982                 if (!is_file($this->_logoPath)) {
983                     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);
984                     $this->_logoPath = false;
985                 }
986             }
987         }
988
989
990         return array_merge(array(
991             'branding'          => array(
992                 'logo'              => $this->_logoPath,
993                 'title'             => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_TITLE},
994                 'description'       => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_DESCRIPTION},
995                 'weburl'            => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_WEBURL},
996             ),
997             'export'            => array(
998                 'timestamp'         => $this->_exportTimeStamp,
999                 'account'           => Tinebase_Core::getUser(),
1000                 'contact'           => Addressbook_Controller_Contact::getInstance()->get(Tinebase_Core::getUser()
1001                                         ->contact_id),
1002                 'groupdata'         => $this->_lastGroupValue,
1003             ),
1004             'additionalRecords' => $this->_additionalRecords,
1005         ), $context);
1006     }
1007
1008     /**
1009      * @param string $_key
1010      * @param string $_value
1011      */
1012     abstract protected function _setValue($_key, $_value);
1013
1014     /**
1015      * @param string $_value
1016      */
1017     abstract protected function _writeValue($_value);
1018
1019     /**
1020      * @param mixed $_value
1021      * @return string
1022      */
1023     protected function _convertToString($_value)
1024     {
1025         if (is_null($_value)) {
1026             $_value = '';
1027         }
1028
1029         if ($_value instanceof DateTime) {
1030             $_value = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_value, null, null,
1031                 $this->_config->datetimeformat);
1032         }
1033
1034         if (is_object($_value) && method_exists($_value, '__toString')) {
1035             $_value = $_value->__toString();
1036         }
1037
1038         if (!is_scalar($_value)) {
1039             $_value = '';
1040         }
1041
1042         return (string)$_value;
1043     }
1044
1045     protected function _endRow()
1046     {
1047     }
1048
1049     /**
1050      * set generic data
1051      *
1052      * @param array $result
1053      */
1054     protected function _onAfterExportRecords(/** @noinspection PhpUnusedParameterInspection */ array $result)
1055     {
1056         $this->_iterationDone = true;
1057
1058         if (null !== $this->_twigTemplate) {
1059             $this->_renderTwigTemplate();
1060         }
1061     }
1062 }