adds directory scanning for apps as fallback
[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      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2010-2011 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
21 {
22     /**
23      * default export definition name
24      * 
25      * @var string
26      */
27     protected $_defaultExportname = 'default';
28         
29     /**
30      * the record controller
31      *
32      * @var Tinebase_Controller_Record_Abstract
33      */
34     protected $_controller = NULL;
35     
36     /**
37      * translation object
38      *
39      * @var Zend_Translate
40      */
41     protected $_translate;
42     
43     /**
44      * locale object
45      *
46      * @var Zend_Locale
47      */
48     protected $_locale;
49
50     /**
51      * export config
52      *
53      * @var Zend_Config_Xml
54      */
55     protected $_config = array();
56     
57     /**
58      * fields with special treatment in addBody
59      *
60      * @var array
61      */
62     protected $_specialFields = array();
63     
64     /**
65      * @var string application name of this export class
66      */
67     protected $_applicationName = 'Tinebase';
68     
69     /**
70      * the record model
71      *
72      * @var string
73      */
74     protected $_modelName = NULL;
75     
76     /**
77      * filter to generate export for
78      * 
79      * @var Tinebase_Model_Filter_FilterGroup
80      */
81     protected $_filter = NULL;
82     
83     /**
84      * sort records by this field (array keys: sort / dir / ...)
85      *
86      * @var array
87      * @see Tinebase_Model_Pagination
88      */
89     protected $_sortInfo = array();
90     
91     /**
92      * preference key if users can have different export configs
93      * 
94      * @var string
95      */
96     protected $_prefKey = NULL;
97     
98     /**
99      * format strings
100      * 
101      * @var string
102      */
103     protected $_format = NULL;
104     
105     /**
106      * other resolved records
107      *
108      * @var array of Tinebase_Record_RecordSet
109      */
110     protected $_resolvedRecords = array();
111     
112     /**
113      * user fields to resolve
114      * 
115      * @var array
116      */
117     protected $_userFields = array('created_by', 'last_modified_by', 'account_id');
118     
119     /**
120      * other fields to resolve
121      * 
122      * @var array
123      */
124     protected $_resolvedFields = array('created_by', 'last_modified_by', 'container_id', 'tags', 'notes', 'relation');
125     
126     /**
127      * get record relations
128      * 
129      * @var boolean
130      */
131     protected $_getRelations = FALSE;
132     
133     /**
134      * custom field names for this model
135      * 
136      * @var array
137      */
138     protected $_customFieldNames = NULL;
139
140     /**
141      * holds resolved records for matrices. this is an array holding each recordset on 
142      * a property with the same name as the field identifier.
143      * 
144      * @var array
145      */
146     protected $_matrixRecords = NULL;
147     
148     /**
149      * the constructor
150      *
151      * @param Tinebase_Model_Filter_FilterGroup $_filter
152      * @param Tinebase_Controller_Record_Interface $_controller (optional)
153      * @param array $_additionalOptions (optional) additional options
154      */
155     public function __construct(Tinebase_Model_Filter_FilterGroup $_filter, Tinebase_Controller_Record_Interface $_controller = NULL, $_additionalOptions = array())
156     {
157         $this->_modelName = $_filter->getModelName();
158         $this->_filter = $_filter;
159         $this->_controller = ($_controller !== NULL) ? $_controller : Tinebase_Core::getApplicationInstance($this->_applicationName, $this->_modelName);
160         $this->_translate = Tinebase_Translation::getTranslation($this->_applicationName);
161         $this->_config = $this->_getExportConfig($_additionalOptions);
162         $this->_locale = Tinebase_Core::get(Tinebase_Core::LOCALE);
163         if (isset($_additionalOptions['sortInfo'])) {
164             if (isset($_additionalOptions['sortInfo']['field'])) {
165                 $this->_sortInfo['sort'] = $_additionalOptions['sortInfo']['field'];
166                 $this->_sortInfo['dir'] = isset($_additionalOptions['sortInfo']['direction']) ? $_additionalOptions['sortInfo']['direction'] : 'ASC';
167             } else {
168                 $this->_sortInfo =  $_additionalOptions['sortInfo'];
169             }
170         }
171     }
172     
173     /**
174      * generate export
175      * 
176      * @return mixed filename/generated object/...
177      */
178     abstract public function generate();
179
180     /**
181      * output to stdout
182      *
183      * @return void
184      */
185 //    abstract public function write();
186
187     /**
188      * @param string|ressource $file
189      */
190 //    abstract public function save($file);
191
192     /**
193      * get custom field names for this app
194      * 
195      * @return array
196      */
197     protected function _getCustomFieldNames()
198     {
199         if ($this->_customFieldNames === NULL) {
200             $this->_customFieldNames = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication($this->_applicationName, $this->_modelName)->name;
201         }
202         
203         return $this->_customFieldNames;
204     }
205     
206     /**
207      * get export format string (csv, ...)
208      * 
209      * @return string
210      */
211     public function getFormat()
212     {
213         if ($this->_format === NULL) {
214             throw new Tinebase_Exception_NotFound('Format string not found.');
215         }
216         
217         return $this->_format;
218     }
219     
220     /**
221      * get download content type
222      * 
223      * @return string
224      */
225     abstract public function getDownloadContentType();
226     
227     /**
228      * return download filename
229      * 
230      * @param string $_appName
231      * @param string $_format
232      */
233     public function getDownloadFilename($_appName, $_format)
234     {
235         return 'export_' . strtolower($_appName) . '.' . $_format;
236     }
237
238     /**
239      * export records
240      */
241     protected function _exportRecords()
242     {
243         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
244             . ' Starting export of ' . $this->_modelName . ' with filter: ' . print_r($this->_filter->toArray(), true)
245             . ' and sort info: ' . print_r($this->_sortInfo, true));
246         
247         $iterator = new Tinebase_Record_Iterator(array(
248             'iteratable' => $this,
249             'controller' => $this->_controller,
250             'filter'     => $this->_filter,
251             'options'     => array(
252                 'searchAction' => 'export',
253                 'sortInfo'     => $this->_sortInfo,
254                 'getRelations' => $this->_getRelations,
255             ),
256         ));
257         
258         $result = $iterator->iterate();
259         
260         $this->_onAfterExportRecords($result);
261         
262         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
263             . ' Exported ' . $result['totalcount'] . ' records.');
264     }
265     
266     /**
267      * template method, gets called after _exportRecords
268      * 
269      * @param array $result
270      */
271     protected function _onAfterExportRecords($result)
272     {
273         
274     }
275     
276     /**
277      * resolve records and prepare for export (set user timezone, ...)
278      *
279      * @param Tinebase_Record_RecordSet $_records
280      */
281     protected function _resolveRecords(Tinebase_Record_RecordSet $_records)
282     {
283         // get field types/identifiers from config
284         $identifiers = array();
285         if ($this->_config->columns) {
286             $types = array();
287             foreach ($this->_config->columns->column as $column) {
288                 $types[] = $column->type;
289                 $identifiers[] = $column->identifier;
290             }
291             $types = array_unique($types);
292         } else {
293             $types = $this->_resolvedFields;
294         }
295
296         // resolve users
297         foreach ($this->_userFields as $field) {
298             if (in_array($field, $types) || in_array($field, $identifiers)) {
299                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resolving users for ' . $field);
300                 Tinebase_User::getInstance()->resolveMultipleUsers($_records, $field, TRUE);
301             }
302         }
303
304         // add notes
305         if (in_array('notes', $types)) {
306             Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($_records, 'notes', 'Sql', FALSE);
307         }
308         
309         // add container
310         if (in_array('container_id', $types)) {
311             Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
312         }
313         
314         $_records->setTimezone(Tinebase_Core::getUserTimezone());
315     }
316
317     /**
318      * return template filename if set
319      * 
320      * @return string|NULL
321      */
322     protected function _getTemplateFilename()
323     {
324         $templateFile = $this->_config->get('template', NULL);
325         if ($templateFile !== NULL) {
326             
327             // check if template file has absolute path
328             if (strpos($templateFile, '/') !== 0) {
329                 $templateFile = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . $this->_applicationName . 
330                     DIRECTORY_SEPARATOR . 'Export' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . $templateFile;
331             }
332             if (file_exists($templateFile)) {
333                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Using template file "' . $templateFile . '" for ' . $this->_modelName . ' export.');
334             } else {
335                 throw new Tinebase_Exception_NotFound('Template file ' . $templateFile . ' not found');
336             }
337         }
338         
339         return $templateFile;
340     }
341     
342     /**
343      * get export config
344      *
345      * @param array $_additionalOptions additional options
346      * @return Zend_Config_Xml
347      * @throws Tinebase_Exception_NotFound
348      */
349     protected function _getExportConfig($_additionalOptions = array())
350     {
351         if ((isset($_additionalOptions['definitionFilename']) || array_key_exists('definitionFilename', $_additionalOptions))) {
352             // get definition from file
353             $definition = Tinebase_ImportExportDefinition::getInstance()->getFromFile(
354                 $_additionalOptions['definitionFilename'],
355                 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)->getId() 
356             );
357             
358         } else if ((isset($_additionalOptions['definitionId']) || array_key_exists('definitionId', $_additionalOptions))) {
359             $definition = Tinebase_ImportExportDefinition::getInstance()->get($_additionalOptions['definitionId']);
360             
361         } else {
362             // get preference from db and set export definition name
363             $exportName = $this->_defaultExportname;
364             if ($this->_prefKey !== NULL) {
365                 $exportName = Tinebase_Core::getPreference($this->_applicationName)->getValue($this->_prefKey, $exportName);
366             }
367             
368             // get export definition by name / model
369             $filter = new Tinebase_Model_ImportExportDefinitionFilter(array(
370                 array('field' => 'model', 'operator' => 'equals', 'value' => $this->_modelName),
371                 array('field' => 'name',  'operator' => 'equals', 'value' => $exportName),
372             ));
373             $definitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
374             if (count($definitions) == 0) {
375                 throw new Tinebase_Exception_NotFound('Export definition for model ' . $this->_modelName . ' not found.');
376             }
377             $definition = $definitions->getFirstRecord();
378             
379             if (! empty($definition->filename)) {
380                 // check if file with plugin options exists and use that
381                 // TODO: this is confusing when imported an extra definition from a file having the same name as the default -> the default will be used
382                 $completeFilename = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . $this->_applicationName . 
383                     DIRECTORY_SEPARATOR . 'Export' . DIRECTORY_SEPARATOR . 'definitions' . DIRECTORY_SEPARATOR . $definition->filename;
384                 try {
385                     $fileDefinition = Tinebase_ImportExportDefinition::getInstance()->getFromFile(
386                         $completeFilename,
387                         Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)->getId() 
388                     );
389                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Using definition from file ' . $definition->filename);
390                     $definition->plugin_options = $fileDefinition->plugin_options;
391                 } catch (Tinebase_Exception_NotFound $tenf) {
392                     Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $tenf->getMessage());
393                 }
394             }
395         }
396         
397         $config = Tinebase_ImportExportDefinition::getInstance()->getOptionsAsZendConfigXml($definition, $_additionalOptions);
398         
399         $this->_addMatrices($config);
400         
401         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
402             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' export config: ' . print_r($config->toArray(), TRUE));
403         }
404         
405         return $config;
406     }
407     
408     /**
409      * 
410      * @param Zend_Config_Xml $config
411      * @param Zend_Config_Xml $fieldConfig
412      * @throws Tinebase_Exception_Data
413      */
414     protected function _addMatrixHeaders(Zend_Config_Xml $config, Zend_Config $fieldConfig)
415     {
416         switch ($fieldConfig->type) {
417             case 'tags':
418                 $filter = new Tinebase_Model_TagFilter(array('application' => $this->_applicationName));
419                 $tags = Tinebase_Tags::getInstance()->searchTags($filter);
420                 
421                 $count = $config->columns->column->count();
422                 
423                 foreach($tags as $tag) {
424                     $cfg = new Zend_Config(array($count => array('identifier' => $tag->name, 'type' => 'tags', 'isMatrixField' => TRUE)));
425                     $config->columns->column->merge($cfg);
426                     $count++;
427                 }
428                 
429                 $this->_matrixRecords['tags'] = $tags;
430                 
431                 break;
432             default:
433                 throw new Tinebase_Exception_Data('Other types than tags are not supported at the moment.');
434         }
435     }
436     
437     /**
438      * if there are matrix fields configured, add them as columns to config
439      * 
440      * @param Zend_Config_Xml $config
441      */
442     protected function _addMatrices(Zend_Config_Xml $config)
443     {
444         if (! isset($config->columns) || ! isset($config->columns->column)) {
445             return;
446         }
447         
448         for ($i = 0; $i < $config->columns->column->count(); $i++) {
449             $column = $config->columns->column->{$i};
450             if ($column && $column->separateColumns) {
451                 $this->_addMatrixHeaders($config, $config->columns->column->{$i});
452                 unset($config->columns->column->{$i});
453             }
454         }
455     }
456
457     /**
458      * return tag names of record
459      * 
460      * @param Tinebase_Record_Abstract $_record
461      * @return string
462      */
463     protected function _getTags(Tinebase_Record_Abstract $_record)
464     {
465         $tags = Tinebase_Tags::getInstance()->getTagsOfRecord($_record);
466         return implode(', ', $tags->name);
467     }
468     
469     /**
470      * return translated Keyfield string
471      *
472      * @param String $_property
473      * @param String $_keyfield
474      * @param String $_application
475      * @return string
476      */
477     protected function _getResolvedKeyfield($_property, $_keyfield, $_application)
478     {
479         $i18nApplication = Tinebase_Translation::getTranslation($_application);
480         $config = Tinebase_Config::getAppConfig($_application);
481
482         $keyfieldConfig = $config->get($_keyfield);
483         if ($keyfieldConfig) {
484             $result = $keyfieldConfig->records->getById($_property);
485         } else {
486             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' '
487                 . ' Could not find keyfield: ' . $_keyfield);
488         }
489         return isset($result) && isset($result->value) ? $i18nApplication->translate($result->value) : $_property;
490     }
491     
492     /**
493      * return shortened field
494      * might run as maxcharacters or maxlines or both
495      * will add "..." to shortened content
496      *
497      * @param String $_property
498      * @param String $_config
499      * @param String $_modus
500      * @return string
501      */
502     protected function _getShortenedField($_property, $_config, $_modus)
503     {
504         $result = $_property;
505         
506         if ($_modus == 'maxcharacters') {
507             $result = substr($result, 0, $_config);
508         }
509         
510         if ($_modus == 'maxlines') {
511             $lines = explode("\n", $result);
512             if(count($lines) > $_config) {
513                 $result = '';
514                 $lines = array_splice($lines, 0, $_config);
515                 foreach($lines as $line) {
516                     $result = $result . $line . "\n";
517                 }
518             }
519         }
520         
521         if ($result != $_property) {
522             $result = $result . '...';
523         }
524         
525         return $result;
526     }
527     
528     /**
529      * 
530      * return container name (or other field)
531      * 
532      * @param Tinebase_Record_Abstract $_record
533      * @param string $_field
534      * @param string $_property
535      * @return string
536      */
537     protected function _getContainer(Tinebase_Record_Abstract $_record, $_field = 'id', $_property = 'container_id')
538     {
539         $container = $_record->{$_property};
540         return $container[$_field];
541     }
542     
543     /**
544      * add relation values from related records
545      * 
546      * @param Tinebase_Record_Abstract $record
547      * @param string $relationType
548      * @param string $recordField
549      * @param boolean $onlyFirstRelation
550      * @return string
551      */
552     protected function _addRelations(Tinebase_Record_Abstract $record, $relationType, $recordField = NULL, $onlyFirstRelation = FALSE, $keyfield = NULL, $application = NULL)
553     {
554         $record->relations->addIndices(array('type'));
555         $matchingRelations = $record->relations->filter('type', $relationType);
556         
557         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
558             . 'Found ' . count($matchingRelations) . ' relations of type ' . $relationType . ' (' . $recordField . ')');
559         
560         $resultArray = array();
561         foreach ($matchingRelations as $relation) {
562             if ($recordField !== NULL) {
563                 if ($keyfield !== NULL && $application !== NULL) {
564                     // special case where we want to translate a keyfield
565                     $result = $this->_getResolvedKeyfield($relation->related_record->{$recordField}, $keyfield, $application);
566                 } else {
567                     $result = $relation->related_record->{$recordField};
568                 }
569                 $resultArray[] = $result;
570             } else {
571                 $resultArray[] = $this->_getRelationSummary($relation->related_record);
572             }
573             
574             if ($onlyFirstRelation) {
575                 break;
576             }
577         }
578         
579         $result = implode(';', $resultArray);
580         
581         return $result;
582     }
583     
584     /**
585      * add relation summary (such as n_fileas, title, ...)
586      * 
587      * @param Tinebase_Record_Abstract $_record
588      * @param string $_type
589      * @return string
590      */
591     protected function _getRelationSummary(Tinebase_Record_Abstract $_record)
592     {
593         $result = '';
594         switch(get_class($_record)) {
595             case 'Addressbook_Model_Contact':
596                 $result = $_record->n_fileas;
597                 break;
598             case 'Tasks_Model_Task':
599                 $result = $_record->summary;
600                 break;
601         }
602         
603         return $result;
604     }
605     
606     /**
607      * add relation values from related records
608      * 
609      * @param Tinebase_Record_Abstract $_record
610      * @param string $_fieldName
611      * @param string $_recordField
612      * @return string
613      */
614     protected function _addNotes(Tinebase_Record_Abstract $_record)
615     {
616         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_record->notes->toArray(), true));
617         
618         $resultArray = array();
619         foreach ($_record->notes as $note) {
620             $date = Tinebase_Translation::dateToStringInTzAndLocaleFormat($note->creation_time);
621             $resultArray[] = $date . ' - ' . $note->note;
622         }
623         
624         $result = implode(';', $resultArray);
625         return $result;
626     }
627
628     /**
629      * get special field value / overwrite this to add special values
630      *
631      * @param Tinebase_Record_Interface $_record
632      * @param array $_param
633      * @param string || null $_key [may be used by child methods e.g. {@see Timetracker_Export_Abstract::_getSpecialFieldValue)]
634      * @param string $_cellType
635      * @return string
636      */
637     protected function _getSpecialFieldValue(Tinebase_Record_Interface $_record, $_param, $_key = NULL, &$_cellType = NULL)
638     {
639         return '';
640     }
641
642     /**
643      * replace and match strings in value
644      * 
645      * @param string $_value
646      * @param Zend_Config $_fieldConfig
647      * @return string
648      */
649     protected function _replaceAndMatchvalue($_value, Zend_Config $_fieldConfig)
650     {
651         $value = $_value;
652         
653         // check for replacements
654         if (isset($_fieldConfig->replace) && isset($_fieldConfig->replace->patterns) && isset($_fieldConfig->replace->replacements)) {
655             //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_fieldConfig->replace->patterns->toArray(), true));
656             //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_fieldConfig->replace->replacements->toArray(), true));
657             $patterns =     (count($_fieldConfig->replace->patterns->pattern) > 1)          
658                 ? $_fieldConfig->replace->patterns->pattern->toArray()          
659                 : $_fieldConfig->replace->patterns->toArray();
660             $replacements = (count($_fieldConfig->replace->replacements->replacement) > 1)  
661                 ? $_fieldConfig->replace->replacements->replacement->toArray()  
662                 : $_fieldConfig->replace->replacements->toArray();
663             $value = preg_replace($patterns, $replacements, $value);
664         }
665
666         // check for matches
667         if (isset($_fieldConfig->match)) {
668             //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_fieldConfig->match, true));
669             preg_match($_fieldConfig->match, $value, $matches);
670             $value = (isset($matches[1])) ? $matches[1] : '';
671         }
672         
673         return $value;
674     }
675
676     /**
677      * get field config by name
678      *
679      * @param  string $fieldName
680      * @return Zend_Config
681      */
682     public function getFieldConfig($fieldName)
683     {
684         foreach($this->_config->columns->column as $column) {
685             if ($column->identifier == $fieldName) {
686                 return $column;
687             }
688         }
689     }
690 }