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