39ed021f42dfd06b3238b3e31ad2e71812e7863d
[tine20] / tine20 / Tinebase / Frontend / Json / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Application
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  */
11
12 /**
13  * Abstract class for an Tine 2.0 application with Json interface
14  * Each tine application must extend this class to gain an native tine 2.0 user
15  * interface.
16  *
17  * @package     Tinebase
18  * @subpackage  Application
19  */
20 abstract class Tinebase_Frontend_Json_Abstract extends Tinebase_Frontend_Abstract implements Tinebase_Frontend_Json_Interface
21 {
22     /**
23      * get totalcount from controller
24      */
25     const TOTALCOUNT_CONTROLLER  = 'controller';
26
27     /**
28      * get totalcount by just counting resultset
29      */
30     const TOTALCOUNT_COUNTRESULT = 'countresult';
31
32     /**
33      * default model (needed for application starter -> defaultContentType)
34      * @var string
35      */
36     protected $_defaultModel = NULL;
37     
38     /**
39      * All configured models
40      * @var array
41      */
42     protected $_configuredModels = NULL;
43     
44     /**
45      * Returns registry data of the application.
46      *
47      * Each application has its own registry to supply static data to the client.
48      * Registry data is queried only once per session from the client.
49      *
50      * This registry must not be used for rights or ACL purposes. Use the generic
51      * rights and ACL mechanisms instead!
52      *
53      * @return mixed array 'variable name' => 'data'
54      */
55     public function getRegistryData()
56     {
57         return array();
58     }
59
60     /**
61      * Returns all relatable models for this app
62      * 
63      * @return array
64      */
65     public function getRelatableModels()
66     {
67         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
68                     . ' This method is deprecated and will be removed. Please use Tinebase_ModelFactory with Tinebase_ModelConfiguration!');
69         $ret = array();
70         
71         if (property_exists($this, '_relatableModels') && is_array($this->_relatableModels)) {
72             // iterates relatable models of this app
73             foreach ($this->_relatableModels as $model) {
74                 
75                 $ownModel = explode('_Model_', $model);
76                 
77                 if (! class_exists($model) || $ownModel[0] != $this->_applicationName) {
78                     continue;
79                 }
80                 $cItems = $model::getRelatableConfig();
81                 
82                 $ownModel = $ownModel[1];
83                 
84                 if (is_array($cItems)) {
85                     foreach($cItems as $cItem) {
86                         
87                         if (! (isset($cItem['config']) || array_key_exists('config', $cItem))) {
88                             continue;
89                         }
90                         
91                         // own side
92                         $ownConfigItem = $cItem;
93                         $ownConfigItem['ownModel'] = $ownModel;
94                         $ownConfigItem['ownApp'] = $this->_applicationName;
95                         
96                         $foreignConfigItem = array(
97                             'reverted'     => true,
98                             'ownApp'       => $cItem['relatedApp'],
99                             'ownModel'     => $cItem['relatedModel'], 
100                             'relatedModel' => $ownModel,
101                             'relatedApp'   => $this->_applicationName,
102                             'default'      => (isset($cItem['default']) || array_key_exists('default', $cItem)) ? $cItem['default'] : NULL
103                         );
104                         
105                         // KeyfieldConfigs
106                         if ((isset($cItem['keyfieldConfig']) || array_key_exists('keyfieldConfig', $cItem))) {
107                             $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
108                             if ($cItem['keyfieldConfig']['from']){
109                                 $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
110                             }
111                         }
112                         
113                         $j=0;
114                         foreach ($cItem['config'] as $conf) {
115                             $max = explode(':',$conf['max']);
116                             $ownConfigItem['config'][$j]['max'] = $max[0];
117                             
118                             $foreignConfigItem['config'][$j] = $conf;
119                             $foreignConfigItem['config'][$j]['max'] = $max[1];
120                             if ($conf['degree'] == 'sibling') {
121                                 $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
122                             } else {
123                                 $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
124                             }
125                             $j++;
126                         }
127                         
128                         $ret[] = $ownConfigItem;
129                         $ret[] = $foreignConfigItem;
130                     }
131                 }
132             }
133         }
134         
135         return $ret;
136     }
137     
138     /**
139      * returns model configurations for application starter
140      * @return array
141      */
142     public function getModelsConfiguration()
143     {
144         $ret = NULL;
145         if ($this->_configuredModels) {
146             foreach ($this->_configuredModels as $modelName) {
147                 $recordClass = $this->_applicationName . '_Model_' . $modelName;
148                 $ret[$modelName] = $recordClass::getConfiguration()->getFrontendConfiguration();
149             }
150         }
151         
152         return $ret;
153     }
154     
155     /**
156      * returns the default model
157      * @return NULL
158      */
159     public function getDefaultModel()
160     {
161         if ($this->_defaultModel) {
162             return $this->_defaultModel;
163         }
164         if ($this->_configuredModels) {
165             return $this->_configuredModels[0];
166         }
167         return NULL;
168     }
169     
170     /**
171      * resolve containers and tags
172      *
173      * @param Tinebase_Record_RecordSet $_records
174      * @param array $_resolveProperties
175      * 
176      * @todo rename function as this no longer resolves users!
177      */
178     public static function resolveContainerTagsUsers(Tinebase_Record_RecordSet $_records, $_resolveProperties = array('container_id', 'tags'))
179     {
180         $firstRecord = $_records->getFirstRecord();
181         
182         if ($firstRecord) {
183             if ($firstRecord->has('container_id') && in_array('container_id', $_resolveProperties)) {
184                 Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
185             }
186             
187             if ($firstRecord->has('tags') && in_array('tags', $_resolveProperties)) {
188                 Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($_records);
189             }
190         }
191     }
192     
193     /************************** protected functions **********************/
194
195     /**
196      * Return a single record
197      *
198      * @param   string $_uid
199      * @param   Tinebase_Controller_Record_Interface $_controller the record controller
200      * @return  array record data
201      */
202     protected function _get($_uid, Tinebase_Controller_Record_Interface $_controller)
203     {
204         $record = $_controller->get($_uid);
205         return $this->_recordToJson($record);
206     }
207
208     /**
209      * Returns all records
210      *
211      * @param   Tinebase_Controller_Record_Interface $_controller the record controller
212      * @return  array record data
213      *
214      * @todo    add sort/dir params here?
215      * @todo    add translation here? that is needed for example for getSalutations() in the addressbook
216      */
217     protected function _getAll(Tinebase_Controller_Record_Interface $_controller)
218     {
219         $records = $_controller->getAll();
220
221         return array(
222             'results'       => $records->toArray(),
223             'totalcount'    => count($records)
224         );
225     }
226
227     /**
228      * Search for records matching given arguments
229      *
230      * @param string|array                        $_filter json encoded / array
231      * @param string|array                        $_paging json encoded / array
232      * @param Tinebase_Controller_SearchInterface $_controller the record controller
233      * @param string                              $_filterModel the class name of the filter model to use
234      * @param bool                                $_getRelations
235      * @param string                              $_totalCountMethod
236      * @return array
237      */
238     protected function _search($_filter, $_paging, Tinebase_Controller_SearchInterface $_controller, $_filterModel, $_getRelations = FALSE, $_totalCountMethod = self::TOTALCOUNT_CONTROLLER)
239     {
240         $decodedPagination = is_array($_paging) ? $_paging : Zend_Json::decode($_paging);
241         $pagination = new Tinebase_Model_Pagination($decodedPagination);
242         $filter = $this->_decodeFilter($_filter, $_filterModel);
243
244         $records = $_controller->search($filter, $pagination, $_getRelations);
245
246         $result = $this->_multipleRecordsToJson($records, $filter);
247
248         return array(
249             'results'       => array_values($result),
250             'totalcount'    => $_totalCountMethod == self::TOTALCOUNT_CONTROLLER ?
251                 $_controller->searchCount($filter) :
252                 count($result),
253             'filter'        => $filter->toArray(TRUE),
254         );
255     }
256
257     /**
258      * decodes the filter string
259      *
260      * @param string|array $_filter
261      * @param string $_filterModel the class name of the filter model to use
262      * @param boolean $_throwExceptionIfEmpty
263      * @return Tinebase_Model_Filter_FilterGroup
264      */
265     protected function _decodeFilter($_filter, $_filterModel, $_throwExceptionIfEmpty = FALSE)
266     {
267         $decodedFilter = is_array($_filter) || strlen($_filter) == 40 ? $_filter : Zend_Json::decode($_filter);
268
269         if (is_array($decodedFilter)) {
270             $filter = new $_filterModel(array());
271             $filter->setFromArrayInUsersTimezone($decodedFilter);
272         } else if (!empty($decodedFilter) && strlen($decodedFilter) == 40) {
273             $filter = Tinebase_PersistentFilter::getFilterById($decodedFilter);
274         } else if ($_throwExceptionIfEmpty) {
275             throw new Tinebase_Exception_InvalidArgument('Filter must not be empty!');
276         } else {
277             $filter = new $_filterModel(array());
278         }
279
280         return $filter;
281     }
282
283     /**
284      * creates/updates a record
285      *
286      * @param   $_recordData
287      * @param   Tinebase_Controller_Record_Interface $_controller the record controller
288      * @param   $_modelName for example: 'Task' for Tasks_Model_Task or the full model name (like 'Tinebase_Model_Container')
289      * @param   $_identifier of the record (default: id)
290      * @param   array $_additionalArguments
291      * @return  array created/updated record
292      */
293     protected function _save($_recordData, Tinebase_Controller_Record_Interface $_controller, $_modelName, $_identifier = 'id', $_additionalArguments = array())
294     {
295         $modelClass = (preg_match('/_Model_/', $_modelName)) ? $_modelName : $this->_applicationName . "_Model_" . $_modelName;
296         $record = new $modelClass(array(), TRUE);
297         $record->setFromJsonInUsersTimezone($_recordData);
298         
299         // if there are dependent records, set the timezone of them and add them to a recordSet
300         $this->_dependentRecordsFromJson($record);
301         
302         $method = (empty($record->$_identifier)) ? 'create' : 'update';
303         $args = array_merge(array($record), $_additionalArguments);
304         $savedRecord = call_user_func_array(array($_controller, $method), $args);
305
306         return $this->_recordToJson($savedRecord);
307     }
308
309     /**
310      * creates recordsets for depedent records or records instead of arrays for records on record fields
311      * and sets timezone of these records to utc
312      * 
313      * @param Tinebase_Record_Abstract $record
314      */
315     protected function _dependentRecordsFromJson(&$record)
316     {
317         $config = $record::getConfiguration();
318         if ($config) {
319             
320             $recordsFields = $config->recordsFields;
321             
322             if ($recordsFields) {
323                 foreach (array_keys($recordsFields) as $property) {
324                     $rcn = $recordsFields[$property]['config']['recordClassName'];
325                     if ($record->has($property) && $record->{$property}) {
326                         $recordSet = new Tinebase_Record_RecordSet($rcn);
327                         foreach ($record->{$property} as $recordArray) {
328                             $srecord = new $rcn(array(), true);
329                             $srecord->setFromJsonInUsersTimezone($recordArray);
330                             $recordSet->addRecord($srecord);
331                         }
332                         $record->{$property} = $recordSet;
333                     }
334                 }
335             }
336         }
337     }
338     
339     /**
340      * update multiple records
341      *
342      * @param string $_filter json encoded filter
343      * @param string $_data json encoded key/value pairs
344      * @param Tinebase_Controller_Record_Interface $_controller
345      * @param string $_filterModel FilterGroup name
346      * @return array with number of updated records
347      */
348     protected function _updateMultiple($_filter, $_data, Tinebase_Controller_Record_Interface $_controller, $_filterModel)
349     {
350         $this->_longRunningRequest();
351         $decodedData   = is_array($_data) ? $_data : Zend_Json::decode($_data);
352         $filter = $this->_decodeFilter($_filter, $_filterModel, TRUE);
353         
354         $result = $_controller->updateMultiple($filter, $decodedData);
355         
356         $result['results']     = $this->_multipleRecordsToJson($result['results']);
357         $result['exceptions']  = $this->_multipleRecordsToJson($result['exceptions']);
358         
359         return $result;
360     }
361     
362     /**
363      * prepare long running request
364      * - execution time
365      * - session write close
366      * 
367      * @param integer $executionTime
368      * @return integer old max execution time
369      */
370     protected function _longRunningRequest($executionTime = 0)
371     {
372         $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(0);
373         // close session to allow other requests
374         Zend_Session::writeClose(true);
375         
376         return $oldMaxExcecutionTime;
377     }
378     
379     /**
380      * update properties of record by id
381      *
382      * @param string $_id record id
383      * @param array  $_data key/value pairs with fields to update
384      * @param Tinebase_Controller_Record_Interface $_controller
385      * @return updated record
386      */
387     protected function _updateProperties($_id, $_data, Tinebase_Controller_Record_Interface $_controller)
388     {
389         $record = $_controller->get($_id);
390
391         // merge with new properties
392         foreach ($_data as $field => $value) {
393             $record->$field = $value;
394         }
395
396         $savedRecord = $_controller->update($record);
397
398         return $this->_recordToJson($savedRecord);
399     }
400     
401     /**
402      * deletes existing records
403      *
404      * @param array|string $_ids
405      * @param Tinebase_Controller_Record_Interface $_controller the record controller
406      * @param array $_additionalArguments
407      * @return array
408      */
409     protected function _delete($_ids, Tinebase_Controller_Record_Interface $_controller, $additionalArguments = array())
410     {
411         if (! is_array($_ids) && strpos($_ids, '[') !== false) {
412             $_ids = Zend_Json::decode($_ids);
413         }
414         $args = array_merge(array($_ids), $additionalArguments);
415         $savedRecord = call_user_func_array(array($_controller, 'delete'), $args);
416
417         return array(
418             'status'    => 'success'
419         );
420     }
421     
422     /**
423      * import records
424      *
425      * @param string $_tempFileId to import
426      * @param string $_importDefinitionId
427      * @param array $_options additional import options
428      * @param array $_clientRecordData
429      * @return array
430      * @throws Tinebase_Exception_NotFound
431      */
432     protected function _import($_tempFileId, $_importDefinitionId, $_options = array(), $_clientRecordData = array())
433     {
434         $definition = Tinebase_ImportExportDefinition::getInstance()->get($_importDefinitionId);
435         $importer = call_user_func($definition->plugin . '::createFromDefinition', $definition, $_options);
436
437         if (! is_object($importer)) {
438             throw new Tinebase_Exception_NotFound('No importer found for ' . $definition->name);
439         }
440
441         // extend execution time to 30 minutes
442         $this->_longRunningRequest(1800);
443
444         $file = Tinebase_TempFile::getInstance()->getTempFile($_tempFileId);
445         $importResult = $importer->importFile($file->path, $_clientRecordData);
446
447         $importResult['results']    = $importResult['results']->toArray();
448         $importResult['exceptions'] = $importResult['exceptions']->toArray();
449         $importResult['status']     = 'success';
450
451         return $importResult;
452     }
453
454     /**
455      * deletes existing records by filter
456      *
457      * @param string $_filter json encoded filter
458      * @param Tinebase_Controller_Record_Interface $_controller the record controller
459      * @param string $_filterModel the class name of the filter model to use
460      * @return array
461      */
462     protected function _deleteByFilter($_filter, Tinebase_Controller_Record_Interface $_controller, $_filterModel)
463     {
464         $filter = $this->_decodeFilter($_filter, $_filterModel, TRUE);
465         
466         // extend execution time to 30 minutes
467         $this->_longRunningRequest(1800);
468
469         $_controller->deleteByFilter($filter);
470         return array(
471             'status'    => 'success'
472         );
473     }
474
475     /**
476      * returns record prepared for json transport
477      *
478      * @param Tinebase_Record_Interface $_record
479      * @return array record data
480      */
481     protected function _recordToJson($_record)
482     {
483         $converter = Tinebase_Convert_Factory::factory($_record);
484         $result = $converter->fromTine20Model($_record);
485
486         return $result;
487     }
488
489     /**
490      * returns multiple records prepared for json transport
491      *
492      * @param Tinebase_Record_RecordSet $_records Tinebase_Record_Abstract
493      * @param Tinebase_Model_Filter_FilterGroup $_filter
494      * @param Tinebase_Model_Pagination $_pagination
495      * @return array data
496      */
497     protected function _multipleRecordsToJson(Tinebase_Record_RecordSet $_records, $_filter = NULL, $_pagination = NULL)
498     {
499         $result = array();
500
501         if ($_records->getFirstRecord()) {
502             $converter = Tinebase_Convert_Factory::factory($_records->getFirstRecord());
503             $result = $converter->fromTine20RecordSet($_records, $_filter, $_pagination);
504         }
505
506         return $result;
507     }
508
509     /**
510      * get available templates by containerId
511      * 
512      * @param integer $containerId
513      * @return array
514      */
515     public function getTemplates($containerId = NULL)
516     {
517         if (! $containerId) {
518             throw new Tinebase_Exception_InvalidArgument('A container id must be set!');
519         }
520     
521         try {
522             $nodes = Tinebase_FileSystem::getInstance()->getNodesByContainer($containerId);
523             $result = $this->_multipleRecordsToJson($nodes);
524         } catch (Exception $e) {
525             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
526                 . ' Could not get template files: ' . $e);
527             $result = array();
528         }
529     
530         return array(
531             'totalcount' => count($result),
532             'results'    => $result,
533         );
534     }
535 }