Tinebase_Export - add twig function relationTranslateModel
[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         $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('relationTranslateModel', function ($model) {
577             // TODO implement this!
578             return $model;
579         }));
580
581         $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName);
582     }
583
584     /**
585      * @return string
586      */
587     public function _getTwigSource()
588     {
589         $source = '[';
590         if (true !== $this->_hasTemplate && $this->_config->columns && $this->_config->columns->column) {
591             foreach ($this->_config->columns->column as $column) {
592                 if ($column->twig) {
593                     $source .= ($source!=='' ? ',"' : '""') . (string)$column->twig . '"';
594                 }
595             }
596         }
597         return $source . ']';
598     }
599
600     /**
601      * @return int
602      */
603     protected function _getLastModifiedTimeStamp()
604     {
605         return filemtime($this->_templateFileName);
606     }
607
608     protected function _getCurrentState()
609     {
610         return array(
611             '_firstIteration'       => $this->_firstIteration,
612             '_writeGenericHeader'   => $this->_writeGenericHeader,
613             '_groupByProperty'      => $this->_groupByProperty,
614             '_groupByProcessor'     => $this->_groupByProcessor,
615             '_lastGroupValue'       => $this->_lastGroupValue,
616             '_currentRecord'        => $this->_currentRecord,
617             '_currentRowType'       => $this->_currentRowType,
618             '_twigTemplate'         => $this->_twigTemplate,
619             '_twigMapping'          => $this->_twigMapping,
620         );
621     }
622
623     protected function _setCurrentState(array $array)
624     {
625         foreach ($array as $key => $value) {
626             $this->{$key} = $value;
627         }
628     }
629
630     /**
631      * add body rows
632      *
633      * @param Tinebase_Record_RecordSet|array $_records
634      */
635     public function processIteration($_records)
636     {
637         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' iterating over export data...');
638
639         if (is_array($_records)) {
640
641             foreach ($_records as $key => $value) {
642                 $this->_startDataSource($key);
643
644                 $this->processIteration($value);
645
646                 $this->_endDataSource($key);
647             }
648
649             return;
650         }
651
652         $this->_resolveRecords($_records);
653
654         if (true === $this->_firstIteration && true === $this->_writeGenericHeader) {
655             $this->_writeGenericHead();
656         }
657
658         $first = $this->_firstIteration;
659         foreach ($_records as $record) {
660             if (null !== $this->_groupByProperty) {
661                 $propertyValue = $record->{$this->_groupByProperty};
662                 if (null !== $this->_groupByProcessor) {
663                     /** @var closure $fn */
664                     $fn = $this->_groupByProcessor;
665                     $fn($propertyValue);
666                 }
667                 if (true === $first || $this->_lastGroupValue !== $propertyValue) {
668                     $this->_lastGroupValue = $propertyValue;
669                     if (false === $first) {
670                         $this->_endGroup();
671                     }
672                     $this->_currentRecord = $record;
673                     $this->_startGroup();
674                 }
675                 // TODO fix this?
676                 //$this->_writeGroupHeading($record);
677             }
678             $this->_currentRecord = $record;
679
680             $this->_currentRowType = self::ROW_TYPE_RECORD;
681
682             $this->_startRow();
683
684             $this->_processRecord($record);
685
686             $this->_endRow();
687
688             if (true === $first) {
689                 $first = false;
690             }
691         }
692
693         if ($_records->count() > 0 && null !== $this->_groupByProperty) {
694             $this->_endGroup();
695         }
696
697         $this->_firstIteration = false;
698     }
699
700     /**
701      * @param $_name
702      */
703     protected function _startDataSource($_name)
704     {
705     }
706
707     /**
708      * @param $_name
709      */
710     protected function _endDataSource($_name)
711     {
712     }
713
714     protected function _startGroup()
715     {
716     }
717
718     protected function _endGroup()
719     {
720     }
721
722     protected function _writeGroupHeading(Tinebase_Record_Interface $_record)
723     {
724         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writting group heading...');
725
726         $this->_currentRowType = self::ROW_TYPE_GROUP_HEADER;
727
728         $this->_startRow();
729
730         $this->_writeValue($_record->{$this->_groupByProperty});
731
732         $this->_endRow();
733     }
734
735     /**
736      * resolve records and prepare for export (set user timezone, ...)
737      *
738      * @param Tinebase_Record_RecordSet $_records
739      */
740     protected function _resolveRecords(Tinebase_Record_RecordSet $_records)
741     {
742         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' resolving export records...');
743         if ($_records->count() === 0) {
744             return;
745         }
746         $record = $_records->getFirstRecord();
747         // FIXME think what to do
748         // TODO fix ALL this!
749
750         // get field types/identifiers from config
751         $identifiers = array();
752         if ($this->_config->columns) {
753             $types = array();
754             foreach ($this->_config->columns->column as $column) {
755                 $types[] = $column->type;
756                 $identifiers[] = $column->identifier;
757             }
758             $types = array_unique($types);
759         } else {
760             $types = $this->_resolvedFields;
761         }
762
763         // resolve users
764         foreach ($this->_userFields as $field) {
765             if (in_array($field, $types) || in_array($field, $identifiers)) {
766                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resolving users for ' . $field);
767                 Tinebase_User::getInstance()->resolveMultipleUsers($_records, $field, true);
768             }
769         }
770
771         // add notes
772         if (in_array('notes', $types)) {
773             Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($_records, 'notes', 'Sql', false);
774         }
775
776         // add container
777         if (in_array('container_id', $types)) {
778             Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
779         }
780
781         if ($record->has('customfields')) {
782             $_records->customfields = array();
783             Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($_records, true);
784         }
785
786         if ($record->has('relations')) {
787             /** @var Tinebase_Record_Abstract $modelName */
788             $modelName = $_records->getRecordClassName();
789
790             $relations = Tinebase_Relations::getInstance()->getMultipleRelations($modelName, 'Sql',
791                 $_records->getArrayOfIds());
792
793             $appConfig = Tinebase_Config::factory($this->_applicationName);
794             $modelConfig = $modelName::getConfiguration();
795
796             /** @var Tinebase_Record_Abstract $record */
797             foreach ($_records as $idx => $record) {
798                 if (isset($relations[$idx])) {
799                     $record->relations = $relations[$idx];
800                 }
801
802                 if (null === $modelConfig) {
803                     foreach ($this->_keyFields as $name => $keyField) {
804                         /** @var Tinebase_Config_KeyField $keyField */
805                         $keyField = $appConfig->{$keyField};
806                         $record->{$name} = $keyField->getTranslatedValue($record->{$name});
807                     }
808
809                     foreach ($this->_virtualFields as $name => $virtualField) {
810                         $value = null;
811                         if (!empty($record->relations)) {
812                             /** @var Tinebase_Model_Relation $relation */
813                             foreach ($record->relations as $relation) {
814                                 if ($relation->related_model === $virtualField['relatedModel'] &&
815                                     $relation->related_degree === $virtualField['relatedDegree'] &&
816                                     $relation->type === $virtualField['type']
817                                 ) {
818                                     $value = $relation->related_record;
819                                     break;
820                                 }
821                             }
822                         }
823                         $record->{$name} = $value;
824                     }
825
826                     foreach ($this->_foreignIdFields as $name => $controller) {
827                         if (!empty($record->{$name})) {
828                             /** @var Tinebase_Controller_Record_Abstract $controller */
829                             $controller = $controller::getInstance();
830                             $record->{$name} = $controller->get($record->{$name});
831                         }
832                     }
833                 } else {
834                     foreach ($modelConfig->virtualFields as $field) {
835                         // resolve virtual relation record from relations property
836                         if (isset($field['type']) && $field['type'] === 'relation') {
837                             $fc = $field['config'];
838                             if (!empty($record->relations)) {
839                                 foreach ($record->relations as $relation) {
840                                     if ($relation->type === $fc['type'] &&
841                                             $relation->related_model === ($fc['appName'] . '_Model_' . $fc['modelName'])) {
842                                         $record->{$field['key']} = $relation->related_record;
843                                     }
844                                 }
845                             }
846                         }
847                     }
848                 }
849             }
850         }
851
852         $_records->setTimezone(Tinebase_Core::getUserTimezone());
853     }
854
855     protected function _writeGenericHead()
856     {
857         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' writing generic header...');
858
859         $this->_currentRowType = self::ROW_TYPE_GENERIC_HEADER;
860
861         $this->_startRow();
862
863         if ($this->_config->columns && $this->_config->columns->column) {
864             foreach ($this->_config->columns->column as $column) {
865                 if ($column->header) {
866                     $this->_writeValue($column->header);
867                 } elseif ($column->recordProperty) {
868                     $this->_writeValue($column->recordProperty);
869                 } else {
870                     $this->_writeValue('');
871                 }
872             }
873         } else {
874             /** @var Tinebase_Record_Abstract $record */
875             $record = new $this->_modelName(array(), true);
876
877             foreach ($record->getFields() as $field) {
878                 // TODO translate?
879                 $this->_writeValue($field);
880             }
881         }
882
883         $this->_endRow();
884     }
885
886     protected function _startRow()
887     {
888     }
889
890     /**
891      * @param Tinebase_Record_Interface $_record
892      */
893     protected function _processRecord(Tinebase_Record_Interface $_record)
894     {
895         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' processing a export record...');
896
897         if (true === $this->_dumpRecords) {
898             foreach ($_record->getFields() as $field) {
899                 $this->_writeValue($this->_convertToString($_record->{$field}));
900             }
901         } elseif (true !== $this->_hasTemplate) {
902             $twigResult = array();
903             if (null !== $this->_twigTemplate) {
904                 $result = json_decode($this->_twigTemplate->render(
905                     $this->_getTwigContext(array('record' => $_record))));
906                 if (is_array($result)) {
907                     $twigResult = $result;
908                 } else {
909                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
910                         ' twig render and json_decode did not return an array: ' . print_r($result, true));
911                 }
912             }
913             $twigCounter = 0;
914             foreach ($this->_config->columns->column as $column) {
915                 if ($column->twig) {
916                     if (isset($twigResult[$twigCounter]) || array_key_exists($twigCounter, $twigResult)) {
917                         $this->_writeValue($this->_convertToString($twigResult[$twigCounter]));
918                     } else {
919                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
920                             ' twig column: ' . $column->twig . ' not found in twig result array');
921                         $this->_writeValue('');
922                     }
923                 } elseif ($column->recordProperty) {
924                     $this->_writeValue($this->_convertToString($_record->{$column->recordProperty}));
925                 } else {
926                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
927                         ' pointless column found: ' . print_r($column, true));
928                 }
929             }
930         } elseif (null !== $this->_twigTemplate) {
931             $this->_renderTwigTemplate($_record);
932         } else {
933             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' can not process record, misconfigured!');
934         }
935     }
936
937     /**
938      * @param Tinebase_Record_Interface|null $_record
939      */
940     protected function _renderTwigTemplate($_record = null)
941     {
942         $twigResult = $this->_twigTemplate->render(
943             $this->_getTwigContext(array('record' => $_record)));
944         $twigResult = json_decode($twigResult);
945         if (!is_array($twigResult)) {
946             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
947                 ' twig render and json_decode did not return an array: ' . print_r($twigResult, true));
948             return;
949         }
950
951         foreach ($this->_twigMapping as $key => $twigKey) {
952             if (isset($twigResult[$key]) || array_key_exists($key, $twigResult)) {
953                 $value = $this->_convertToString($twigResult[$key]);
954             } else {
955                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
956                     ' twig mapping: ' . $key . ' ' . $twigKey . ' not found in twig result array');
957                 $value = '';
958             }
959             $this->_setValue($twigKey, $value);
960         }
961     }
962
963     /**
964      * @param array $context
965      * @return array
966      */
967     protected function _getTwigContext(array $context)
968     {
969
970         if (null === $this->_logoPath) {
971             $this->_logoPath = Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_LOGO};
972
973             if (strpos($this->_logoPath, '://') === false) {
974                 if ('.' === $this->_logoPath[0] && '/' === $this->_logoPath[1]) {
975                     $this->_logoPath = mb_substr($this->_logoPath, 1);
976                 } elseif ('/' !== $this->_logoPath[0]) {
977                     $this->_logoPath = '/' . $this->_logoPath;
978                 }
979
980                 $baseDir = dirname(dirname(__DIR__));
981                 if (0 === strpos($this->_logoPath, $baseDir)) {
982                     $this->_logoPath = 'file://' . $this->_logoPath;
983                 } else {
984                     $this->_logoPath = 'file://' . $baseDir . $this->_logoPath;
985                 }
986
987                 if (!is_file($this->_logoPath)) {
988                     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);
989                     $this->_logoPath = false;
990                 }
991             }
992         }
993
994
995         return array_merge(array(
996             'branding'          => array(
997                 'logo'              => $this->_logoPath,
998                 'title'             => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_TITLE},
999                 'description'       => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_DESCRIPTION},
1000                 'weburl'            => Tinebase_Config::getInstance()->{Tinebase_Config::BRANDING_WEBURL},
1001             ),
1002             'export'            => array(
1003                 'timestamp'         => $this->_exportTimeStamp,
1004                 'account'           => Tinebase_Core::getUser(),
1005                 'contact'           => Addressbook_Controller_Contact::getInstance()->get(Tinebase_Core::getUser()
1006                                         ->contact_id),
1007                 'groupdata'         => $this->_lastGroupValue,
1008             ),
1009             'additionalRecords' => $this->_additionalRecords,
1010         ), $context);
1011     }
1012
1013     /**
1014      * @param string $_key
1015      * @param string $_value
1016      */
1017     abstract protected function _setValue($_key, $_value);
1018
1019     /**
1020      * @param string $_value
1021      */
1022     abstract protected function _writeValue($_value);
1023
1024     /**
1025      * @param mixed $_value
1026      * @return string
1027      */
1028     protected function _convertToString($_value)
1029     {
1030         if (is_null($_value)) {
1031             $_value = '';
1032         }
1033
1034         if ($_value instanceof DateTime) {
1035             $_value = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_value, null, null,
1036                 $this->_config->datetimeformat);
1037         }
1038
1039         if (is_object($_value) && method_exists($_value, '__toString')) {
1040             $_value = $_value->__toString();
1041         }
1042
1043         if (!is_scalar($_value)) {
1044             $_value = '';
1045         }
1046
1047         return (string)$_value;
1048     }
1049
1050     protected function _endRow()
1051     {
1052     }
1053
1054     /**
1055      * set generic data
1056      *
1057      * @param array $result
1058      */
1059     protected function _onAfterExportRecords(/** @noinspection PhpUnusedParameterInspection */ array $result)
1060     {
1061         $this->_iterationDone = true;
1062
1063         if (null !== $this->_twigTemplate) {
1064             $this->_renderTwigTemplate();
1065         }
1066     }
1067 }