0012202: saving timesheet results in segfault
[tine20] / tine20 / Tinebase / Convert / Json.php
1 <?php
2 /**
3  * convert functions for records from/to json (array) format
4  * 
5  * @package     Tinebase
6  * @subpackage  Convert
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) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * convert functions for records from/to json (array) format
14  *
15  * @package     Tinebase
16  * @subpackage  Convert
17  */
18 class Tinebase_Convert_Json implements Tinebase_Convert_Interface
19 {
20     /**
21      * converts external format to Tinebase_Record_Abstract
22      * 
23      * @param  mixed                     $_blob   the input data to parse
24      * @param  Tinebase_Record_Abstract  $_record  update existing record
25      * @return Tinebase_Record_Abstract
26      */
27     public function toTine20Model($_blob, Tinebase_Record_Abstract $_record = NULL)
28     {
29         throw new Tinebase_Exception_NotImplemented('From json to record is not implemented yet');
30     }
31     
32     /**
33      * converts Tinebase_Record_Abstract to external format
34      * 
35      * @param  Tinebase_Record_Abstract $_record
36      * @return mixed
37      */
38     public function fromTine20Model(Tinebase_Record_Abstract $_record)
39     {
40         if (! $_record) {
41             return array();
42         }
43         
44         // for resolving we'll use recordset
45         $recordClassName = get_class($_record);
46         $records = new Tinebase_Record_RecordSet($recordClassName, array($_record));
47         $modelConfiguration = $recordClassName::getConfiguration();
48         
49         $this->_resolveBeforeToArray($records, $modelConfiguration, FALSE);
50         
51         $_record = $records->getFirstRecord();
52         $_record->setTimezone(Tinebase_Core::getUserTimezone());
53         $_record->bypassFilters = true;
54         
55         $result = $_record->toArray();
56         
57         $result = $this->_resolveAfterToArray($result, $modelConfiguration, FALSE);
58         
59         return $result;
60     }
61
62     /**
63      * resolves single record fields (Tinebase_ModelConfiguration._recordsFields)
64      * 
65      * @param Tinebase_Record_RecordSet $_records the records
66      * @param Tinebase_ModelConfiguration $modelConfig
67      */
68     protected function _resolveSingleRecordFields(Tinebase_Record_RecordSet $_records, $modelConfig = NULL)
69     {
70         if (! $modelConfig) {
71             return;
72         }
73         
74         $resolveFields = $modelConfig->recordFields;
75         
76         if ($resolveFields && is_array($resolveFields)) {
77             // don't search twice if the same recordClass gets resolved on multiple fields
78             $resolveRecords = array();
79             foreach ($resolveFields as $fieldKey => $fieldConfig) {
80                 $resolveRecords[$fieldConfig['config']['recordClassName']][] = $fieldKey;
81             }
82             
83             foreach ($resolveRecords as $foreignRecordClassName => $fields) {
84                 $foreignIds = array();
85                 $foreignRecordsArray = array();
86                 $fields = (array) $fields;
87                 foreach ($fields as $field) {
88                     $idsForField = $_records->{$field};
89                     foreach ($idsForField as $key => $value) {
90                         if ($value instanceof Tinebase_Record_Abstract) {
91                             $foreignRecordsArray[$value->getId()] = $value;
92                         } else {
93                             if ($value && !isset($foreignRecordsArray[$value])) {
94                                 $foreignIds[$value] = $value;
95                             }
96                         }
97                     }
98                 }
99                 
100                 if (empty($foreignIds) && empty($foreignRecordsArray)) {
101                     continue;
102                 }
103                 
104                 $cfg = $resolveFields[$fields[0]];
105
106                 if ($cfg['type'] == 'user') {
107                     $foreignRecords = Tinebase_User::getInstance()->getMultiple($foreignIds);
108                 } else if ($cfg['type'] == 'container') {
109                     // TODO: resolve recursive records of records better in controller
110                     // TODO: resolve containers
111                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
112                         . ' No handling for container foreign records implemented');
113
114                     //$foreignRecords = new Tinebase_Record_RecordSet('Tinebase_Model_Container');
115                     // $foreignRecords->addRecord(Tinebase_Container::getInstance()->get(XXX));
116                     continue;
117                 } else {
118                     try {
119                         $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
120                         $foreignRecords = $controller->getMultiple($foreignIds);
121                     } catch (Tinebase_Exception_AccessDenied $tead) {
122                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
123                             . ' No right to access application of record ' . $foreignRecordClassName);
124                         continue;
125                     }
126                 }
127
128                 foreach($foreignRecordsArray as $id => $rec) {
129                     if ($foreignRecords->getById($id) === false) {
130                         $foreignRecords->addRecord($rec);
131                     }
132                 }
133                 
134                 if ($foreignRecords->count() === 0) {
135                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
136                         . ' No matching foreign records found (' . $foreignRecordClassName . ')');
137                     continue;
138                 }
139                 $foreignRecords->setTimezone(Tinebase_Core::getUserTimezone());
140                 $foreignRecords->convertDates = true;
141                 Tinebase_Frontend_Json_Abstract::resolveContainersAndTags($foreignRecords);
142                 $fr = $foreignRecords->getFirstRecord();
143                 if ($fr && $fr->has('notes')) {
144                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
145                 }
146                 
147                 foreach ($_records as $record) {
148                     foreach ($fields as $field) {
149                         $foreignId = $record->{$field};
150                         if (is_scalar($foreignId)) {
151                             $idx = $foreignRecords->getIndexById($foreignId);
152                             if (isset($idx) && $idx !== FALSE) {
153                                 $record->{$field} = $foreignRecords[$idx];
154                             } else {
155                                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
156                                     . ' No matching foreign record found for id: ' . $foreignId);
157                             }
158                         }
159                     }
160                 }
161             }
162         }
163     }
164     
165     /**
166      * resolves multiple records (fallback)
167      * 
168      * @deprecated use Tinebase_ModelConfiguration to configure your models, so this won't be used anymore 
169      * @param Tinebase_Record_RecordSet $_records the records
170      * @param array $resolveFields
171      */
172     public static function resolveMultipleIdFields($records, $resolveFields = NULL)
173     {
174         if (! $records instanceof Tinebase_Record_RecordSet || !$records->count()) {
175             return;
176         }
177         
178         $ownRecordClass = $records->getRecordClassName();
179         if ($resolveFields === NULL) {
180             $resolveFields = $ownRecordClass::getResolveForeignIdFields();
181         }
182         
183         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
184             . ' Resolving ' . $ownRecordClass . ' fields: ' . print_r($resolveFields, TRUE));
185         
186         foreach ((array) $resolveFields as $foreignRecordClassName => $fields) {
187             if ($foreignRecordClassName === 'recursive') {
188                 foreach ($fields as $field => $model) {
189                     foreach ($records->$field as $subRecords) {
190                         self::resolveMultipleIdFields($subRecords);
191                     }
192                 }
193             } else {
194                 self::_resolveForeignIdFields($records, $foreignRecordClassName, (array) $fields);
195             }
196         }
197     }
198     
199     /**
200      * resolve foreign fields for records like user ids to users, etc.
201      * 
202      * @param Tinebase_Record_RecordSet $records
203      * @param string $foreignRecordClassName
204      * @param array $fields
205      */
206     protected static function _resolveForeignIdFields($records, $foreignRecordClassName, $fields)
207     {
208         $options = (isset($fields['options']) || array_key_exists('options', $fields)) ? $fields['options'] : array();
209         $fields = (isset($fields['fields']) || array_key_exists('fields', $fields)) ? $fields['fields'] : $fields;
210         
211         $foreignIds = array();
212         foreach ($fields as $field) {
213             $foreignIds = array_unique(array_merge($foreignIds, $records->{$field}));
214         }
215         
216         try {
217             $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
218         } catch (Tinebase_Exception_AccessDenied $tead) {
219             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
220                 . ' No right to access application of record ' . $foreignRecordClassName);
221             return;
222         }
223         
224         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
225             . ' Fetching ' . $foreignRecordClassName . ' by id: ' . print_r($foreignIds, TRUE));
226         
227         if ((isset($options['ignoreAcl']) || array_key_exists('ignoreAcl', $options)) && $options['ignoreAcl']) {
228             // @todo make sure that second param of getMultiple() is $ignoreAcl
229             $foreignRecords = $controller->getMultiple($foreignIds, TRUE);
230         } else {
231             $foreignRecords = $controller->getMultiple($foreignIds);
232         }
233         
234         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
235             . ' Foreign records found: ' . print_r($foreignRecords->toArray(), TRUE));
236         
237         if (count($foreignRecords) === 0) {
238             return;
239         }
240         
241         foreach ($records as $record) {
242             foreach ($fields as $field) {
243                 if (is_scalar($record->{$field})) {
244                     $idx = $foreignRecords->getIndexById($record->{$field});
245                     if (isset($idx) && $idx !== FALSE) {
246                         $record->{$field} = $foreignRecords[$idx];
247                     } else {
248                         switch ($foreignRecordClassName) {
249                             case 'Tinebase_Model_User':
250                             case 'Tinebase_Model_FullUser':
251                                 $record->{$field} = Tinebase_User::getInstance()->getNonExistentUser();
252                                 break;
253                             default:
254                                 // skip
255                         }
256                     }
257                 }
258             }
259         }
260     }
261     
262     /**
263      * resolve multiple record fields (Tinebase_ModelConfiguration._recordsFields)
264      * 
265      * @param Tinebase_Record_RecordSet $_records
266      * @param Tinebase_ModelConfiguration $modelConfiguration
267      * @param boolean $multiple
268      * @throws Tinebase_Exception_UnexpectedValue
269      */
270     protected function _resolveMultipleRecordFields(Tinebase_Record_RecordSet $_records, $modelConfiguration = NULL, $multiple = false)
271     {
272         if (! $modelConfiguration || (! $_records->count())) {
273             return;
274         }
275         
276         if (! ($resolveFields = $modelConfiguration->recordsFields)) {
277             return;
278         }
279         
280         $ownIds = $_records->{$modelConfiguration->idProperty};
281         
282         // iterate fields to resolve
283         foreach ($resolveFields as $fieldKey => $c) {
284             $config = $c['config'];
285             
286             // resolve records, if omitOnSearch is definitively set to FALSE (by default they won't be resolved on search)
287             if ($multiple && !(isset($config['omitOnSearch']) && $config['omitOnSearch'] === FALSE)) {
288                 continue;
289             }
290
291             if (! isset($config['controllerClassName'])) {
292                 throw new Tinebase_Exception_UnexpectedValue('Controller class name needed');
293             }
294
295             // fetch the fields by the refIfField
296             $controller = $config['controllerClassName']::getInstance();
297             $filterName = $config['filterClassName'];
298             
299             $filterArray = array();
300             
301             // addFilters can be added and must be added if the same model resides in more than one records fields
302             if (isset($config['addFilters']) && is_array($config['addFilters'])) {
303                 $filterArray = $config['addFilters'];
304             }
305             
306             $filter = new $filterName($filterArray);
307             $filter->addFilter(new Tinebase_Model_Filter_Id(array('field' => $config['refIdField'], 'operator' => 'in', 'value' => $ownIds)));
308             
309             $paging = NULL;
310             if (isset($config['paging']) && is_array($config['paging'])) {
311                 $paging = new Tinebase_Model_Pagination($config['paging']);
312             }
313             
314             $foreignRecords = $controller->search($filter, $paging);
315             $foreignRecordClass = $foreignRecords->getRecordClassName();
316             $foreignRecordModelConfiguration = $foreignRecordClass::getConfiguration();
317             
318             $foreignRecords->setTimezone(Tinebase_Core::getUserTimezone());
319             $foreignRecords->convertDates = true;
320             
321             $fr = $foreignRecords->getFirstRecord();
322
323             // @todo: resolve alarms?
324             // @todo: use parts parameter?
325             if ($foreignRecordModelConfiguration->resolveRelated && $fr) {
326                 if ($fr->has('notes')) {
327                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
328                 }
329                 if ($fr->has('tags')) {
330                     Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($foreignRecords);
331                 }
332                 if ($fr->has('relations')) {
333                     $relations = Tinebase_Relations::getInstance()->getMultipleRelations($foreignRecordClass, 'Sql', $foreignRecords->{$fr->getIdProperty()} );
334                     $foreignRecords->setByIndices('relations', $relations);
335                 }
336                 if ($fr->has('customfields')) {
337                     Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($foreignRecords);
338                 }
339                 if ($fr->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
340                     Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($foreignRecords);
341                 }
342             }
343             
344             if ($foreignRecords->count() > 0) {
345                 foreach ($_records as $record) {
346                     $filtered = $foreignRecords->filter($config['refIdField'], $record->getId())->toArray();
347                     $filtered = $this->_resolveAfterToArray($filtered, $foreignRecordModelConfiguration, TRUE);
348                     $record->{$fieldKey} = $filtered;
349                 }
350                 
351             } else {
352                 $_records->{$fieldKey} = NULL;
353             }
354         }
355         
356     }
357     
358     /**
359      * resolves virtual fields, if a function has been defined in the field definition
360      * 
361      * @param array $resultSet
362      * @param Tinebase_ModelConfiguration $modelConfiguration
363      * @param boolean $multiple
364      */
365     protected function _resolveVirtualFields($resultSet, $modelConfiguration = NULL, $multiple = false)
366     {
367         if (! $modelConfiguration || ! ($virtualFields = $modelConfiguration->virtualFields)) {
368             return $resultSet;
369         }
370         
371         if ($modelConfiguration->resolveVFGlobally === TRUE) {
372             
373             $controller = $modelConfiguration->getControllerInstance();
374             
375             if ($multiple) {
376                 return $controller->resolveMultipleVirtualFields($resultSet);
377             }
378             return $controller->resolveVirtualFields($resultSet);
379         }
380         
381         foreach ($virtualFields as $field) {
382             // resolve virtual relation record from relations property
383             if (! $multiple && isset($field['type']) && $field['type'] == 'relation') {
384                 $fc = $field['config'];
385                 if (isset($resultSet['relations']) && (is_array($resultSet['relations']))) {
386                     foreach($resultSet['relations'] as $relation) {
387                         if (($relation['type'] == $fc['type']) && ($relation['related_model'] == ($fc['appName'] . '_Model_' . $fc['modelName']))) {
388                             $resultSet[$field['key']] = $relation['related_record'];
389                         }
390                     }
391                 }
392             // resolve virtual field by function
393             } else if ((isset($field['function']) || array_key_exists('function', $field))) {
394                 if (is_array($field['function'])) {
395                     if (count($field['function']) > 1) { // static method call
396                         $class  = $field['function'][0];
397                         $method = $field['function'][1];
398                         $resultSet = $class::$method($resultSet);
399
400                     } else { // use key as classname and value as method name
401                         $ks = array_keys($field['function']);
402                         $class  = array_pop($ks);
403                         $vs = array_values($field['function']);
404                         $method = array_pop($vs);
405                         $class = $class::getInstance();
406                         
407                         $resultSet = $class->$method($resultSet);
408                         
409                     }
410                 // if no array has been given, this should be a function name
411                 } else {
412                     $resolveFunction = $field['function'];
413                     $resultSet = $resolveFunction($resultSet);
414                 }
415             }
416         }
417         
418         return $resultSet;
419     }
420     
421     /**
422      * resolves child records before converting the record set to an array
423      * 
424      * @param Tinebase_Record_RecordSet $records
425      * @param Tinebase_ModelConfiguration $modelConfiguration
426      * @param boolean $multiple
427      */
428     protected function _resolveBeforeToArray($records, $modelConfiguration, $multiple = false)
429     {
430         Tinebase_Frontend_Json_Abstract::resolveContainersAndTags($records);
431         
432         self::resolveMultipleIdFields($records);
433         
434         // use modern record resolving, if the model was configured using Tinebase_ModelConfiguration
435         // at first, resolve all single record fields
436         if ($modelConfiguration) {
437             $this->_resolveSingleRecordFields($records, $modelConfiguration);
438         
439             // resolve all multiple records fields
440             $this->_resolveMultipleRecordFields($records, $modelConfiguration, $multiple);
441         }
442     }
443     
444     /**
445      * resolves child records after converting the record set to an array
446      * 
447      * @param array $result
448      * @param Tinebase_ModelConfiguration $modelConfiguration
449      * @param boolean $multiple
450      * 
451      * @return array
452      */
453     protected function _resolveAfterToArray($result, $modelConfiguration, $multiple = false)
454     {
455         $result = $this->_resolveVirtualFields($result, $modelConfiguration, $multiple);
456         return $result;
457     }
458     
459     /**
460      * converts Tinebase_Record_RecordSet to external format
461      * 
462      * @param Tinebase_Record_RecordSet  $_records
463      * @param Tinebase_Model_Filter_FilterGroup $_filter
464      * @param Tinebase_Model_Pagination $_pagination
465      * 
466      * @return mixed
467      */
468     public function fromTine20RecordSet(Tinebase_Record_RecordSet $_records = NULL, $_filter = NULL, $_pagination = NULL)
469     {
470         if (! $_records || count($_records) == 0) {
471             return array();
472         }
473         
474         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
475             . ' Processing ' . count($_records) . ' records ...');
476         
477         // find out if there is a modelConfiguration
478         $ownRecordClass = $_records->getRecordClassName();
479         $config = $ownRecordClass::getConfiguration();
480         
481         $this->_resolveBeforeToArray($_records, $config, TRUE);
482         
483         $_records->setTimezone(Tinebase_Core::getUserTimezone());
484         $_records->convertDates = true;
485         
486         $result = $_records->toArray();
487         
488         // resolve all virtual fields after converting to array, so we can add these properties "virtually"
489         $result = $this->_resolveAfterToArray($result, $config, TRUE);
490         
491         return $result;
492     }
493 }