3 * Abstract record controller for Tine 2.0 applications
6 * @subpackage Controller
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) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
11 * @todo this should be splitted into smaller parts!
15 * abstract record controller class for Tine 2.0 applications
18 * @subpackage Controller
20 abstract class Tinebase_Controller_Record_Abstract
21 extends Tinebase_Controller_Abstract
22 implements Tinebase_Controller_Record_Interface, Tinebase_Controller_SearchInterface
25 * application backend class
27 * @var Tinebase_Backend_Sql_Interface
36 * @todo perhaps we can remove that and build model name from name of the class (replace 'Controller' with 'Model')
38 protected $_modelName;
41 * check for container ACLs
45 * @todo rename to containerACLChecks
47 protected $_doContainerACLChecks = TRUE;
50 * do right checks - can be enabled/disabled by doRightChecks
54 protected $_doRightChecks = TRUE;
57 * use notes - can be enabled/disabled by useNotes
61 protected $_setNotes = TRUE;
64 * delete or just set is_delete=1 if record is going to be deleted
65 * - legacy code -> remove that when all backends/applications are using the history logging
69 protected $_purgeRecords = TRUE;
72 * omit mod log for this records
76 protected $_omitModLog = FALSE;
79 * resolve customfields in search()
83 protected $_resolveCustomFields = FALSE;
86 * clear customfields cache on create / update
90 protected $_clearCustomFieldsCache = FALSE;
93 * Do we update relation to this record
97 protected $_doRelationUpdate = TRUE;
100 * Do we force sent modlog for this record
104 protected $_doForceModlogInfo = FALSE;
107 * send notifications?
111 protected $_sendNotifications = false;
114 * if some of the relations should be deleted
118 protected $_relatedObjectsToDelete = array();
121 * set this to true to create/update related records
125 protected $_inspectRelatedRecords = false;
128 * set this to true to check (duplicate/freebusy/...) in create/update of related records
132 protected $_doRelatedCreateUpdateCheck = false;
139 protected $_recordAlarmField = 'dtstart';
142 * duplicate check fields / if this is NULL -> no duplicate check
146 protected $_duplicateCheckFields = NULL;
148 protected $_duplicateCheckConfig = array();
151 * holds new relation on update multiple
154 protected $_newRelations = NULL;
157 * holds relations to remove on update multiple
160 protected $_removeRelations = NULL;
163 * result of updateMultiple function
167 protected $_updateMultipleResult = array();
170 * should each record be validated in updateMultiple
171 * - FALSE: only the first record is validated with the incoming data
175 protected $_updateMultipleValidateEachRecord = FALSE;
178 * returns controller for records of given model
180 * @param string $_model
182 public static function getController($_model)
184 list($appName, $i, $modelName) = explode('_', $_model);
185 return Tinebase_Core::getApplicationInstance($appName, $modelName);
189 * returns backend for this controller
192 public function getBackend()
194 return $this->_backend;
197 /*********** get / search / count **************/
200 * get list of records
202 * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
203 * @param Tinebase_Model_Pagination|optional $_pagination
204 * @param boolean|array $_getRelations
205 * @param boolean $_onlyIds
206 * @param string $_action for right/acl check
207 * @return Tinebase_Record_RecordSet|array
209 public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
211 $this->_checkRight($_action);
212 $this->checkFilterACL($_filter, $_action);
213 $this->_addDefaultFilter($_filter);
215 $result = $this->_backend->search($_filter, $_pagination, $_onlyIds);
217 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
218 . ' Got ' . count($result) . ' search results');
221 if ($_getRelations && count($result) > 0 && $result->getFirstRecord()->has('relations')) {
222 // if getRelations is true, all relations should be fetched
223 if ($_getRelations === true) {
224 $_getRelations = NULL;
226 $result->setByIndices('relations', Tinebase_Relations::getInstance()->getMultipleRelations($this->_modelName, $this->_getBackendType(), $result->getId(), NULL, array(), FALSE, $_getRelations));
228 if ($this->resolveCustomfields()) {
229 Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($result);
237 * you can define default filters here
239 * @param Tinebase_Model_Filter_FilterGroup $_filter
241 protected function _addDefaultFilter(Tinebase_Model_Filter_FilterGroup $_filter = NULL)
247 * Gets total count of search with $_filter
249 * @param Tinebase_Model_Filter_FilterGroup $_filter
250 * @param string $_action for right/acl check
253 public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
255 $this->checkFilterACL($_filter, $_action);
257 $count = $this->_backend->searchCount($_filter);
259 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
260 . ' Got ' . (is_array($count) ? print_r($count, 1) : $count) . ' search count');
266 * set/get the sendNotifications state
268 * @param boolean optional
271 public function sendNotifications()
273 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
274 return $this->_setBooleanMemberVar('_sendNotifications', $value);
278 * set/get a boolean member var
280 * @param string $name
281 * @param boolean $value
284 protected function _setBooleanMemberVar($name, $value = NULL)
286 $currValue = $this->{$name};
287 if ($value !== NULL) {
288 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
289 . ' Resetting ' . $name . ' to ' . (int) $value);
290 $this->{$name} = $value;
297 * setter for $relatedObjectsToDelete
299 * @param array $relatedObjectNames
301 public function setRelatedObjectsToDelete(array $relatedObjectNames)
303 $this->_relatedObjectsToDelete = $relatedObjectNames;
307 * set/get purging of record when deleting
309 * @param boolean optional
312 public function purgeRecords()
314 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
315 return $this->_setBooleanMemberVar('_purgeRecords', $value);
319 * set/get checking ACL rights
321 * @param boolean optional
324 public function doContainerACLChecks()
326 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
327 return $this->_setBooleanMemberVar('_doContainerACLChecks', $value);
331 * set/get resolving of customfields
333 * @param boolean optional
336 public function resolveCustomfields()
338 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
339 $currentValue = ($this->_setBooleanMemberVar('_resolveCustomFields', $value)
340 && Tinebase_CustomField::getInstance()->appHasCustomFields($this->_applicationName, $this->_modelName));
341 return $currentValue;
345 * set/get modlog active
347 * @param boolean optional
350 public function modlogActive()
352 if (! $this->_backend) {
353 throw new Tinebase_Exception_NotFound('Backend not defined');
356 $currValue = $this->_backend->getModlogActive();
357 if (func_num_args() === 1) {
358 $paramValue = (bool) func_get_arg(0);
359 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Resetting modlog active to ' . (int) $paramValue);
360 $this->_backend->setModlogActive($paramValue);
361 $this->_omitModLog = ! $paramValue;
368 * set/get relation update
370 * @param boolean optional
373 public function doRelationUpdate()
375 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
376 return $this->_setBooleanMemberVar('_doRelationUpdate', $value);
380 * set/get force modlog info
382 * @param boolean optional
385 public function doForceModlogInfo()
387 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
388 return $this->_setBooleanMemberVar('_doForceModlogInfo', $value);
392 * set/get _inspectRelatedRecords
396 public function doInspectRelatedRecords()
398 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
399 return $this->_setBooleanMemberVar('_inspectRelatedRecords', $value);
403 * set/get duplicateCheckFields
405 * @param array optional
408 public function duplicateCheckFields()
410 if (func_num_args() === 1) {
411 $this->_duplicateCheckFields = func_get_arg(0);
414 return $this->_duplicateCheckFields;
418 * disable this to do not check any rights
420 * @param boolean optional
423 public function doRightChecks()
425 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
426 return $this->_setBooleanMemberVar('_doRightChecks', $value);
433 * @param int $_containerId
434 * @param bool $_getRelatedData
435 * @param bool $_getDeleted
436 * @return Tinebase_Record_Interface
437 * @throws Tinebase_Exception_AccessDenied
439 public function get($_id, $_containerId = NULL, $_getRelatedData = TRUE, $_getDeleted = FALSE)
441 $this->_checkRight('get');
443 if (! $_id) { // yes, we mean 0, null, false, ''
444 $record = new $this->_modelName(array(), true);
446 if ($this->_doContainerACLChecks) {
447 if ($_containerId === NULL) {
448 $containers = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Core::getUser(), Tinebase_Model_Grants::GRANT_ADD);
449 $record->container_id = $containers[0]->getId();
451 $record->container_id = $_containerId;
456 $record = $this->_backend->get($_id, $_getDeleted);
457 $this->_checkGrant($record, 'get');
459 // get related data only on request (defaults to TRUE)
460 if ($_getRelatedData) {
461 $this->_getRelatedData($record);
463 if ($record->has('notes')) {
464 $record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord($this->_modelName, $record->getId());
473 * check if record with given $id exists
478 public function exists($id)
480 $this->_checkRight('get');
483 $record = $this->_backend->get($id);
484 $result = $this->_checkGrant($record, 'get', FALSE);
485 } catch (Tinebase_Exception_NotFound $tenf) {
493 * add related data to record
495 * @param Tinebase_Record_Interface $record
497 protected function _getRelatedData($record)
499 if ($record->has('tags')) {
500 Tinebase_Tags::getInstance()->getTagsOfRecord($record);
502 if ($record->has('relations')) {
503 $record->relations = Tinebase_Relations::getInstance()->getRelations($this->_modelName, $this->_getBackendType(), $record->getId());
505 if ($record->has('alarms')) {
506 $this->getAlarms($record);
508 if ($this->resolveCustomfields()) {
509 Tinebase_CustomField::getInstance()->resolveRecordCustomFields($record);
511 if ($record->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
512 Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachments($record);
517 * Returns a set of records identified by their id's
519 * @param array $_ids array of record identifiers
520 * @param bool $_ignoreACL don't check acl grants
521 * @return Tinebase_Record_RecordSet of $this->_modelName
523 public function getMultiple($_ids, $_ignoreACL = FALSE)
525 $this->_checkRight('get');
527 // get all allowed containers and add them to getMultiple query
528 $containerIds = ($this->_doContainerACLChecks && $_ignoreACL !== TRUE)
529 ? Tinebase_Container::getInstance()->getContainerByACL(
530 Tinebase_Core::getUser(),
531 $this->_applicationName,
532 Tinebase_Model_Grants::GRANT_READ,
535 $records = $this->_backend->getMultiple($_ids, $containerIds);
537 if ($this->resolveCustomfields()) {
538 Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($records);
547 * @param string $_orderBy Order result by
548 * @param string $_orderDirection Order direction - allowed are ASC and DESC
549 * @throws Tinebase_Exception_InvalidArgument
550 * @return Tinebase_Record_RecordSet
552 public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
554 $this->_checkRight('get');
556 $records = $this->_backend->getAll($_orderBy, $_orderDirection);
558 if ($this->resolveCustomfields()) {
559 Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($records);
565 /*************** add / update / delete / move *****************/
570 * @param Tinebase_Record_Interface $_record
571 * @param boolean $_duplicateCheck
572 * @return Tinebase_Record_Interface
573 * @throws Tinebase_Exception_AccessDenied
575 public function create(Tinebase_Record_Interface $_record, $_duplicateCheck = true)
577 $this->_checkRight('create');
579 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
580 . print_r($_record->toArray(),true));
581 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
582 . ' Create new ' . $this->_modelName);
584 $db = (method_exists($this->_backend, 'getAdapter')) ? $this->_backend->getAdapter() : Tinebase_Core::getDb();
587 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
589 // add personal container id if container id is missing in record
590 if ($_record->has('container_id') && empty($_record->container_id)) {
591 $containers = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Core::getUser(), Tinebase_Model_Grants::GRANT_ADD);
592 $_record->container_id = $containers[0]->getId();
595 $_record->isValid(TRUE);
597 $this->_checkGrant($_record, 'create');
599 // added _doForceModlogInfo behavior
600 if ($_record->has('created_by')) {
601 $origRecord = clone ($_record);
602 Tinebase_Timemachine_ModificationLog::setRecordMetaData($_record, 'create');
603 $this->_forceModlogInfo($_record, $origRecord, 'create');
606 $this->_inspectBeforeCreate($_record);
607 if ($_duplicateCheck) {
608 $this->_duplicateCheck($_record);
610 $createdRecord = $this->_backend->create($_record);
611 $this->_inspectAfterCreate($createdRecord, $_record);
612 $this->_setRelatedData($createdRecord, $_record, TRUE);
613 $this->_setNotes($createdRecord, $_record);
615 if ($this->sendNotifications()) {
616 $this->doSendNotifications($createdRecord, Tinebase_Core::getUser(), 'created');
619 $this->_increaseContainerContentSequence($createdRecord, Tinebase_Model_ContainerContent::ACTION_CREATE);
621 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
622 } catch (Exception $e) {
623 $this->_handleRecordCreateOrUpdateException($e);
626 if ($this->_clearCustomFieldsCache) {
627 Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('customfields'));
630 return $this->get($createdRecord);
634 * handle record exception
636 * @param Exception $e
639 * @todo invent hooking mechanism for database/backend independant exception handling (like lock timeouts)
641 protected function _handleRecordCreateOrUpdateException(Exception $e)
643 Tinebase_TransactionManager::getInstance()->rollBack();
645 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
646 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
648 if ($e instanceof Zend_Db_Statement_Exception && preg_match('/Lock wait timeout exceeded/', $e->getMessage())) {
649 throw new Tinebase_Exception_Backend_Database_LockTimeout($e->getMessage());
656 * inspect creation of one record (before create)
658 * @param Tinebase_Record_Interface $_record
661 protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record)
667 * do duplicate check (before create)
669 * @param Tinebase_Record_Interface $_record
671 * @throws Tinebase_Exception_Duplicate
673 protected function _duplicateCheck(Tinebase_Record_Interface $_record)
675 $duplicateFilter = $this->_getDuplicateFilter($_record);
677 if ($duplicateFilter === NULL) {
681 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
682 ' Doing duplicate check.');
684 $duplicates = $this->search($duplicateFilter, new Tinebase_Model_Pagination(array('limit' => 5)), /* $_getRelations = */ true);
686 if (count($duplicates) > 0) {
687 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
688 ' Found ' . count($duplicates) . ' duplicate(s).');
690 $ted = new Tinebase_Exception_Duplicate('Duplicate record(s) found');
691 $ted->setModelName($this->_modelName);
692 $ted->setData($duplicates);
693 $ted->setClientRecord($_record);
696 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
697 ' No duplicates found.');
702 * get duplicate filter
704 * @param Tinebase_Record_Interface $_record
705 * @return Tinebase_Model_Filter_FilterGroup|NULL
707 protected function _getDuplicateFilter(Tinebase_Record_Interface $_record)
709 if (empty($this->_duplicateCheckFields)) {
713 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
714 . ' Duplicate check fields: ' . print_r($this->_duplicateCheckFields, TRUE));
717 foreach ($this->_duplicateCheckFields as $group) {
718 $addFilter = array();
719 if (! is_array($group)) {
720 $group = array($group);
722 foreach ($group as $field) {
723 if (! empty($_record->{$field})) {
724 if ($field === 'relations') {
725 $relationFilter = $this->_getRelationDuplicateFilter($_record);
726 if ($relationFilter) {
727 $addFilter[] = $relationFilter;
730 $addFilter[] = array('field' => $field, 'operator' => 'equals', 'value' => $_record->{$field});
733 } else if (isset($_record->customfields[$field])) {
734 $customFieldConfig = Tinebase_CustomField::getInstance()->getCustomFieldByNameAndApplication($this->_applicationName, $field, $this->_modelName);
735 if ($customFieldConfig) {
736 $addFilter[] = array('field' => 'customfield', 'operator' => 'equals', 'value' => array(
737 'value' => $_record->customfields[$field],
738 'cfId' => $customFieldConfig->getId()
743 if (! empty($addFilter)) {
744 $filters[] = array('condition' => 'AND', 'filters' => $addFilter);
748 if (empty($filters)) {
752 $filterClass = $this->_modelName . 'Filter';
753 $filterData = (count($filters) > 1) ? array(array('condition' => 'OR', 'filters' => $filters)) : $filters;
755 // exclude own record if it has an id
756 $recordId = $_record->getId();
757 if (! empty($recordId)) {
758 $filterData[] = array('field' => 'id', 'operator' => 'notin', 'value' => array($recordId));
761 $filter = new $filterClass($filterData);
763 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
764 . print_r($filter->toArray(), TRUE));
769 protected function _getRelationDuplicateFilter($record)
772 $relations = $record->relations;
774 if (count($relations) === 0 || ! isset($this->_duplicateCheckConfig['relations']['filterField'])) {
778 if (! $relations instanceof Tinebase_Record_RecordSet) {
779 $relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', $relations, /* $_bypassFilters = */ true);
782 // check for relation and add relation filter
783 $type = isset($this->_duplicateCheckConfig['relations']['type']) ? $this->_duplicateCheckConfig['relations']['type'] : '';
784 $relations = $relations->filter('type', $type);
785 if (count($relations) > 0) {
786 $duplicateRelation = $relations->getFirstRecord();
787 if ($duplicateRelation->related_id) {
789 'field' => $this->_duplicateCheckConfig['relations']['filterField'],
791 'value' => array(array('field' => ':id', 'operator' => 'equals', 'value' => $duplicateRelation->related_id))
800 * inspect creation of one record (after create)
802 * @param Tinebase_Record_Interface $_createdRecord
803 * @param Tinebase_Record_Interface $_record
806 protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
811 * increase container content sequence
813 * @param Tinebase_Record_Interface $_record
814 * @param string $action
816 protected function _increaseContainerContentSequence(Tinebase_Record_Interface $record, $action = NULL)
818 if ($record->has('container_id')) {
819 Tinebase_Container::getInstance()->increaseContentSequence($record->container_id, $action, $record->getId());
824 * Force modlog info if set
826 * @param Tinebase_Record_Interface $_record
827 * @param Tinebase_Record_Interface $_origRecord
828 * @param string $_action
831 protected function _forceModlogInfo(Tinebase_Record_Interface $_record, Tinebase_Record_Interface $_origRecord, $_action = NULL)
833 if ($this->_doForceModlogInfo && ! empty($_origRecord)) {
835 if ($_action == 'create') {
836 if (! empty($_origRecord->created_by)) {
837 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - created_by: ' . $_origRecord->created_by);
838 $_record->created_by = $_origRecord->created_by;
840 if (! empty($_origRecord->creation_time)) {
841 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - creation_time: ' . $_origRecord->creation_time);
842 $_record->creation_time = $_origRecord->creation_time;
844 if (! empty($_origRecord->last_modified_by)) {
845 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - last_modified_by: ' . $_origRecord->last_modified_by);
846 $_record->last_modified_by = $_origRecord->last_modified_by;
848 if (! empty($_origRecord->last_modified_time)) {
849 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - last_modified_time: ' . $_origRecord->last_modified_time);
850 $_record->last_modified_time = $_origRecord->last_modified_time;
855 if ($_action == 'update') {
856 if (! empty($_origRecord->last_modified_by)) {
857 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - last_modified_by: ' . $_origRecord->last_modified_by);
858 $_record->last_modified_by = $_origRecord->last_modified_by;
860 if (! empty($_origRecord->last_modified_time)) {
861 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Force modlog - last_modified_time: ' . $_origRecord->last_modified_time);
862 $_record->last_modified_time = $_origRecord->last_modified_time;
871 * @param Tinebase_Record_Interface $_record
872 * @param boolean $_duplicateCheck
873 * @return Tinebase_Record_Interface
874 * @throws Tinebase_Exception_AccessDenied
876 * @todo fix duplicate check on update / merge needs to remove the changed record / ux discussion
878 public function update(Tinebase_Record_Interface $_record, $_duplicateCheck = TRUE)
880 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
881 . ' Record to update: ' . print_r($_record->toArray(), TRUE));
882 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
883 . ' Update ' . $this->_modelName);
885 $db = (method_exists($this->_backend, 'getAdapter')) ? $this->_backend->getAdapter() : Tinebase_Core::getDb();
888 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
890 $_record->isValid(TRUE);
891 $currentRecord = $this->get($_record->getId());
893 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
894 . ' Current record: ' . print_r($currentRecord->toArray(), TRUE));
896 // add _doForceModlogInfo behavior
897 $origRecord = clone ($_record);
898 $this->_updateACLCheck($_record, $currentRecord);
899 $this->_concurrencyManagement($_record, $currentRecord);
900 $this->_forceModlogInfo($_record, $origRecord, 'update');
901 $this->_inspectBeforeUpdate($_record, $currentRecord);
903 // NOTE removed the duplicate check because we can not remove the changed record yet
904 // if ($_duplicateCheck) {
905 // $this->_duplicateCheck($_record);
908 $updatedRecord = $this->_backend->update($_record);
909 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
910 . ' Updated record: ' . print_r($updatedRecord->toArray(), TRUE));
912 $this->_inspectAfterUpdate($updatedRecord, $_record, $currentRecord);
913 $updatedRecordWithRelatedData = $this->_setRelatedData($updatedRecord, $_record, TRUE);
914 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
915 . ' Updated record with related data: ' . print_r($updatedRecordWithRelatedData->toArray(), TRUE));
917 $currentMods = $this->_writeModLog($updatedRecordWithRelatedData, $currentRecord);
918 $this->_setNotes($updatedRecordWithRelatedData, $_record, Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED, $currentMods);
920 if ($this->_sendNotifications && count($currentMods) > 0) {
921 $this->doSendNotifications($updatedRecordWithRelatedData, Tinebase_Core::getUser(), 'changed', $currentRecord);
924 if ($_record->has('container_id') && $currentRecord->container_id !== $updatedRecord->container_id) {
925 $this->_increaseContainerContentSequence($currentRecord, Tinebase_Model_ContainerContent::ACTION_DELETE);
926 $this->_increaseContainerContentSequence($updatedRecord, Tinebase_Model_ContainerContent::ACTION_CREATE);
928 $this->_increaseContainerContentSequence($updatedRecord, Tinebase_Model_ContainerContent::ACTION_UPDATE);
931 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
933 } catch (Exception $e) {
934 $this->_handleRecordCreateOrUpdateException($e);
937 if ($this->_clearCustomFieldsCache) {
938 Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('customfields'));
941 return $this->get($updatedRecord->getId());
945 * do ACL check for update record
947 * @param Tinebase_Record_Interface $_record
948 * @param Tinebase_Record_Interface $_currentRecord
950 protected function _updateACLCheck($_record, $_currentRecord)
952 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
953 . ' Doing ACL check ...');
955 if ($_currentRecord->has('container_id') && $_currentRecord->container_id != $_record->container_id) {
956 $this->_checkGrant($_record, 'create');
957 $this->_checkRight('create');
958 // NOTE: It's not yet clear if we have to demand delete grants here or also edit grants would be fine
959 $this->_checkGrant($_currentRecord, 'delete');
960 $this->_checkRight('delete');
962 $this->_checkGrant($_record, 'update', TRUE, 'No permission to update record.', $_currentRecord);
963 $this->_checkRight('update');
968 * concurrency management & history log
970 * @param Tinebase_Record_Interface $_record
971 * @param Tinebase_Record_Interface $_currentRecord
973 protected function _concurrencyManagement($_record, $_currentRecord)
975 if (! $_record->has('created_by')) {
979 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
980 . ' Doing concurrency check ...');
982 $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
983 $modLog->manageConcurrentUpdates(
984 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)->getId(),
985 $_record, $_currentRecord);
986 $modLog->setRecordMetaData($_record, 'update', $_currentRecord);
994 protected function _getBackendType()
996 $type = (method_exists( $this->_backend, 'getType')) ? $this->_backend->getType() : 'Sql';
1003 * @param Tinebase_Record_Interface $_newRecord
1004 * @param Tinebase_Record_Interface $_oldRecord
1005 * @return NULL|Tinebase_Record_RecordSet
1006 * @throws Tinebase_Exception_InvalidArgument
1008 protected function _writeModLog($_newRecord, $_oldRecord)
1010 if (! is_object($_newRecord)) {
1011 throw new Tinebase_Exception_InvalidArgument('record object expected');
1014 if (! $_newRecord->has('created_by') || $this->_omitModLog === TRUE) {
1018 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1019 . ' Writing modlog for ' . get_class($_newRecord));
1021 $currentMods = Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($_newRecord, $_oldRecord, $this->_modelName, $this->_getBackendType(), $_newRecord->getId());
1023 return $currentMods;
1027 * set relations / tags / alarms
1029 * @param Tinebase_Record_Interface $updatedRecord the just updated record
1030 * @param Tinebase_Record_Interface $record the update record
1031 * @param boolean $returnUpdatedRelatedData
1032 * @return Tinebase_Record_Interface
1034 protected function _setRelatedData($updatedRecord, $record, $returnUpdatedRelatedData = FALSE)
1036 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1037 . ' Update record: ' . print_r($record->toArray(), true));
1039 // relations won't be touched if the property is set to NULL
1040 // an empty array on the relations property will remove all relations
1041 if ($record->has('relations') && isset($record->relations) && is_array($record->relations)) {
1042 $type = $this->_getBackendType();
1043 Tinebase_Relations::getInstance()->setRelations(
1046 $updatedRecord->getId(),
1049 $this->_inspectRelatedRecords,
1050 $this->_doRelatedCreateUpdateCheck);
1052 if ($record->has('tags') && isset($record->tags) && (is_array($record->tags) || $record->tags instanceof Tinebase_Record_RecordSet)) {
1053 $updatedRecord->tags = $record->tags;
1054 Tinebase_Tags::getInstance()->setTagsOfRecord($updatedRecord);
1056 if ($record->has('alarms') && isset($record->alarms)) {
1057 $this->_saveAlarms($record);
1059 if ($record->has('attachments') && isset($record->attachments) && Tinebase_Core::isFilesystemAvailable()) {
1060 $updatedRecord->attachments = $record->attachments;
1061 Tinebase_FileSystem_RecordAttachments::getInstance()->setRecordAttachments($updatedRecord);
1064 if ($returnUpdatedRelatedData) {
1065 $this->_getRelatedData($updatedRecord);
1068 return $updatedRecord;
1074 * @param Tinebase_Record_Interface $_updatedRecord the just updated record
1075 * @param Tinebase_Record_Interface $_record the update record
1076 * @param string $_systemNoteType
1077 * @param Tinebase_Record_RecordSet $_currentMods
1079 protected function _setNotes($_updatedRecord, $_record, $_systemNoteType = Tinebase_Model_Note::SYSTEM_NOTE_NAME_CREATED, $_currentMods = NULL)
1081 if (! $_record->has('notes') || $this->_setNotes === false) {
1085 if (isset($_record->notes) && is_array($_record->notes)) {
1086 $_updatedRecord->notes = $_record->notes;
1087 Tinebase_Notes::getInstance()->setNotesOfRecord($_updatedRecord);
1089 Tinebase_Notes::getInstance()->addSystemNote($_updatedRecord, Tinebase_Core::getUser(), $_systemNoteType, $_currentMods);
1093 * inspect update of one record (before update)
1095 * @param Tinebase_Record_Interface $_record the update record
1096 * @param Tinebase_Record_Interface $_oldRecord the current persistent record
1099 protected function _inspectBeforeUpdate($_record, $_oldRecord)
1104 * inspect update of one record (after update)
1106 * @param Tinebase_Record_Interface $updatedRecord the just updated record
1107 * @param Tinebase_Record_Interface $record the update record
1108 * @param Tinebase_Record_Interface $currentRecord the current record (before update)
1111 protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
1116 * update modlog / metadata / add systemnote for multiple records defined by filter
1118 * NOTE: this should be done in a transaction because of the concurrency handling as
1119 * we want the same seq in the record and in the modlog
1121 * @param Tinebase_Model_Filter_FilterGroup|array $_filterOrIds
1122 * @param array $_oldData
1123 * @param array $_newData
1125 public function concurrencyManagementAndModlogMultiple($_filterOrIds, $_oldData, $_newData)
1127 $ids = ($_filterOrIds instanceof Tinebase_Model_Filter_FilterGroup) ? $this->search($_filterOrIds, NULL, FALSE, TRUE, 'update') : $_filterOrIds;
1128 if (! is_array($ids) || count($ids) === 0) {
1132 if ($this->_omitModLog !== TRUE) {
1133 $recordSeqs = $this->_backend->getPropertyByIds($ids, 'seq');
1135 list($currentAccountId, $currentTime) = Tinebase_Timemachine_ModificationLog::getCurrentAccountIdAndTime();
1136 $updateMetaData = array(
1137 'last_modified_by' => $currentAccountId,
1138 'last_modified_time' => $currentTime,
1139 'seq' => new Zend_Db_Expr('seq + 1'),
1140 'recordSeqs' => $recordSeqs, // is not written to DB yet
1143 $updateMetaData = array();
1146 $this->_backend->updateMultiple($ids, $updateMetaData);
1148 if ($this->_omitModLog !== TRUE && is_object(Tinebase_Core::getUser())) {
1149 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1150 . ' Writing modlog for ' . count($ids) . ' records ...');
1152 $currentMods = Tinebase_Timemachine_ModificationLog::getInstance()->writeModLogMultiple($ids, $_oldData, $_newData, $this->_modelName, $this->_getBackendType(), $updateMetaData);
1153 Tinebase_Notes::getInstance()->addMultipleModificationSystemNotes($currentMods, $currentAccountId, $this->_modelName);
1158 * handles relations on update multiple
1160 * @param string $key
1161 * @param string $value
1162 * @throws Tinebase_Exception_Record_DefinitionFailure
1164 protected function _handleRelationsOnUpdateMultiple($key, $value)
1166 $getRelations = true;
1167 preg_match('/%(.+)-((.+)_Model_(.+))/', $key, $a);
1168 if (count($a) < 4) {
1169 throw new Tinebase_Exception_Record_DefinitionFailure('The relation to delete/set is not configured properly!');
1172 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1173 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Handle relations for ' . $this->_modelName);
1176 $relConfig = Tinebase_Relations::getConstraintsConfigs(array($this->_modelName, $a[2]));
1178 $constraintsConfig = NULL;
1181 foreach ($relConfig as $config) {
1182 if ($config['relatedApp'] == $a[3] && $config['relatedModel'] == $a[4] && isset($config['config']) && is_array($config['config'])) {
1183 foreach ($config['config'] as $constraint) {
1184 if ($constraint['type'] == $a[1]) {
1185 $constraintsConfig = $constraint;
1193 // update multiple is not possible without having a constraints config
1194 if (! $constraintsConfig) {
1195 throw new Tinebase_Exception_Record_DefinitionFailure('No relation definition could be found for this model!');
1199 'own_model' => $this->_modelName,
1200 'own_backend' => 'Sql',
1201 'own_degree' => isset($constraintsConfig['sibling']) ? $constraintsConfig['sibling'] : 'sibling',
1202 'related_model' => $a[2],
1203 'related_backend' => 'Sql',
1204 'type' => isset($constraintsConfig['type']) ? $constraintsConfig['type'] : ' ',
1205 'remark' => isset($constraintsConfig['defaultRemark']) ? $constraintsConfig['defaultRemark'] : ' '
1208 if (! $this->_removeRelations) {
1209 $this->_removeRelations = array($rel);
1211 $this->_removeRelations[] = $rel;
1214 if (! empty($value)) { // delete relations in iterator
1215 $rel['related_id'] = $value;
1216 if (! $this->_newRelations) {
1217 $this->_newRelations = array($rel);
1219 $this->_newRelations[] = $rel;
1223 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
1224 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' New relations: ' . print_r($this->_newRelations, true)
1225 . ' Remove relations: ' . print_r($this->_removeRelations, true));
1229 * update multiple records
1231 * @param Tinebase_Model_Filter_FilterGroup $_filter
1232 * @param array $_data
1233 * @return integer number of updated records
1235 * @todo add param $_returnFullResults (if false, do not return updated records in 'results')
1237 public function updateMultiple($_filter, $_data)
1239 $this->_checkRight('update');
1240 $this->checkFilterACL($_filter, 'update');
1241 $getRelations = false;
1243 $this->_newRelations = NULL;
1244 $this->_removeRelations = NULL;
1246 foreach ($_data as $key => $value) {
1247 if (stristr($key,'#')) {
1248 $_data['customfields'][substr($key,1)] = $value;
1249 unset($_data[$key]);
1251 if (stristr($key, '%')) {
1252 $getRelations = true;
1253 $this->_handleRelationsOnUpdateMultiple($key, $value);
1254 unset($_data[$key]);
1258 $this->_updateMultipleResult = array(
1259 'results' => new Tinebase_Record_RecordSet($this->_modelName),
1260 'exceptions' => new Tinebase_Record_RecordSet('Tinebase_Model_UpdateMultipleException'),
1265 $iterator = new Tinebase_Record_Iterator(array(
1266 'iteratable' => $this,
1267 'controller' => $this,
1268 'filter' => $_filter,
1269 'options' => array('getRelations' => $getRelations),
1270 'function' => 'processUpdateMultipleIteration',
1272 $result = $iterator->iterate($_data);
1274 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1275 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updated ' . $this->_updateMultipleResult['totalcount'] . ' records.');
1278 if ($this->_clearCustomFieldsCache) {
1279 Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('customfields'));
1282 return $this->_updateMultipleResult;
1286 * enable / disable notes
1288 * @param boolean $_value
1291 public function useNotes()
1293 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
1294 return $this->_setBooleanMemberVar('_setNotes', $value);
1300 * @param Tinebase_Record_Abstract $currentRecord
1303 protected function _iterateRelations($currentRecord)
1305 if(! $currentRecord->relations || get_class($currentRecord->relations) != 'Tinebase_Record_RecordSet') {
1306 $currentRecord->relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
1309 $be = new Tinebase_Relation_Backend_Sql();
1311 // handle relations to remove
1312 if($this->_removeRelations) {
1313 if($currentRecord->relations->count()) {
1314 foreach($this->_removeRelations as $remRelation) {
1315 $removeRelations = $currentRecord->relations
1316 ->filter('type', $remRelation['type'])
1317 ->filter('related_model', $remRelation['related_model']);
1319 $currentRecord->relations->removeRecords($removeRelations);
1324 // handle new relations
1325 if($this->_newRelations) {
1326 $removeRelations = NULL;
1327 foreach($this->_newRelations as $newRelation) {
1328 $removeRelations = $currentRecord->relations
1329 ->filter('type', $newRelation['type'])
1330 ->filter('related_model', $newRelation['related_model']);
1332 $already = $removeRelations->filter('related_id', $newRelation['related_id']);
1334 if($already->count() > 0) {
1335 $removeRelations = NULL;
1337 $newRelation['own_id'] = $currentRecord->getId();
1338 $rel = new Tinebase_Model_Relation();
1339 $rel->setFromArray($newRelation);
1340 if ($removeRelations) {
1341 $currentRecord->relations->removeRecords($removeRelations);
1343 $currentRecord->relations->addRecord($rel);
1348 return $currentRecord->relations->toArray();
1352 * update multiple records in an iteration
1353 * @see Tinebase_Record_Iterator / self::updateMultiple()
1355 * @param Tinebase_Record_RecordSet $_records
1356 * @param array $_data
1358 public function processUpdateMultipleIteration($_records, $_data)
1360 if (count($_records) === 0) {
1363 $bypassFilters = FALSE;
1364 foreach ($_records as $currentRecord) {
1365 $oldRecordArray = $currentRecord->toArray();
1366 unset($oldRecordArray['relations']);
1368 $data = array_merge($oldRecordArray, $_data);
1370 if ($this->_newRelations || $this->_removeRelations) {
1371 $data['relations'] = $this->_iterateRelations($currentRecord);
1374 $record = new $this->_modelName($data, $bypassFilters);
1375 $updatedRecord = $this->update($record, FALSE);
1377 $this->_updateMultipleResult['results']->addRecord($updatedRecord);
1378 $this->_updateMultipleResult['totalcount'] ++;
1380 } catch (Tinebase_Exception_Record_Validation $e) {
1381 if ($this->_updateMultipleValidateEachRecord === FALSE) {
1384 $this->_updateMultipleResult['exceptions']->addRecord(new Tinebase_Model_UpdateMultipleException(array(
1385 'id' => $currentRecord->getId(),
1387 'record' => $currentRecord,
1388 'code' => $e->getCode(),
1389 'message' => $e->getMessage()
1391 $this->_updateMultipleResult['failcount'] ++;
1393 if ($this->_updateMultipleValidateEachRecord === FALSE) {
1394 // only validate the first record
1395 $bypassFilters = TRUE;
1401 * Deletes a set of records.
1403 * If one of the records could not be deleted, no record is deleted
1405 * @param array $_ids array of record identifiers
1406 * @return Tinebase_Record_RecordSet
1407 * @throws Tinebase_Exception_NotFound|Tinebase_Exception
1409 public function delete($_ids)
1411 if ($_ids instanceof $this->_modelName) {
1412 $_ids = (array)$_ids->getId();
1415 $ids = $this->_inspectDelete((array) $_ids);
1417 $records = $this->_backend->getMultiple((array)$ids);
1419 if (count((array)$ids) != count($records)) {
1420 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Only ' . count($records) . ' of ' . count((array)$ids) . ' records exist.');
1423 if (empty($records)) {
1427 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1428 . ' Deleting ' . count($records) . ' of ' . $this->_modelName . ' records ...');
1431 $db = $this->_backend->getAdapter();
1432 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1433 $this->_checkRight('delete');
1435 foreach ($records as $record) {
1436 if ($this->sendNotifications()) {
1437 $this->_getRelatedData($record);
1439 $this->_deleteRecord($record);
1442 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1444 // send notifications
1445 if ($this->sendNotifications()) {
1446 foreach ($records as $record) {
1447 $this->doSendNotifications($record, Tinebase_Core::getUser(), 'deleted');
1451 } catch (Exception $e) {
1452 Tinebase_TransactionManager::getInstance()->rollBack();
1453 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . print_r($e->getMessage(), true));
1457 if ($this->_clearCustomFieldsCache) {
1458 Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('customfields'));
1461 // returns deleted records
1466 * delete records by filter
1468 * @param Tinebase_Model_Filter_FilterGroup $_filter
1469 * @return Tinebase_Record_RecordSet
1471 public function deleteByFilter(Tinebase_Model_Filter_FilterGroup $_filter)
1473 $oldMaxExcecutionTime = ini_get('max_execution_time');
1475 Tinebase_Core::setExecutionLifeTime(300); // 5 minutes
1477 $ids = $this->search($_filter, NULL, FALSE, TRUE);
1478 $deletedRecords = $this->delete($ids);
1480 // reset max execution time to old value
1481 Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
1483 return $deletedRecords;
1487 * inspects delete action
1489 * @param array $_ids
1490 * @return array of ids to actually delete
1492 protected function _inspectDelete(array $_ids)
1498 * move records to new container / folder / whatever
1500 * @param mixed $_records (can be record set, filter, array, string)
1501 * @param mixed $_target (string, container record, ...)
1504 public function move($_records, $_target, $_containerProperty = 'container_id')
1506 $records = $this->_convertToRecordSet($_records);
1507 $targetContainerId = ($_target instanceof Tinebase_Model_Container) ? $_target->getId() : $_target;
1509 if ($this->_doContainerACLChecks) {
1510 // check add grant in target container
1511 if (! Tinebase_Core::getUser()->hasGrant($targetContainerId, Tinebase_Model_Grants::GRANT_ADD)) {
1512 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Permission denied to add records to container.');
1513 throw new Tinebase_Exception_AccessDenied('You are not allowed to move records to this container');
1516 // check delete grant in source container
1517 $containerIdsWithDeleteGrant = Tinebase_Container::getInstance()->getContainerByACL(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Model_Grants::GRANT_DELETE, TRUE);
1518 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1519 . ' Containers with delete grant: ' . print_r($containerIdsWithDeleteGrant, true));
1520 foreach ($records as $index => $record) {
1521 if (! in_array($record->{$_containerProperty}, $containerIdsWithDeleteGrant)) {
1522 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1523 . ' Permission denied to remove record ' . $record->getId() . ' from container ' . $record->{$_containerProperty}
1525 unset($records[$index]);
1530 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1531 . ' Moving ' . count($records) . ' ' . $this->_modelName . '(s) to container ' . $targetContainerId);
1533 // move (update container id)
1534 $idsToMove = $records->getArrayOfIds();
1535 $filterClass = $this->_modelName . 'Filter';
1536 if (! class_exists($filterClass)) {
1537 throw new Tinebase_Exception_NotFound('Filter class ' . $filterClass . ' not found!');
1539 $filter = new $filterClass(array(
1540 array('field' => 'id', 'operator' => 'in', 'value' => $idsToMove)
1542 $updateResult = $this->updateMultiple($filter, array(
1543 $_containerProperty => $targetContainerId
1549 /*********** helper funcs **************/
1554 * @param Tinebase_Record_Interface $_record
1555 * @throws Tinebase_Exception_AccessDenied
1557 protected function _deleteRecord(Tinebase_Record_Interface $_record)
1559 $this->_checkGrant($_record, 'delete');
1561 $this->_deleteLinkedObjects($_record);
1563 if (! $this->_purgeRecords && $_record->has('created_by')) {
1564 Tinebase_Timemachine_ModificationLog::setRecordMetaData($_record, 'delete', $_record);
1565 $this->_backend->update($_record);
1567 $this->_backend->delete($_record);
1570 $this->_increaseContainerContentSequence($_record, Tinebase_Model_ContainerContent::ACTION_DELETE);
1574 * delete linked objects (notes, relations, ...) of record
1576 * @param Tinebase_Record_Interface $_record
1578 protected function _deleteLinkedObjects(Tinebase_Record_Interface $_record)
1580 // delete notes & relations
1581 if ($_record->has('notes')) {
1582 Tinebase_Notes::getInstance()->deleteNotesOfRecord($this->_modelName, $this->_getBackendType(), $_record->getId());
1585 if ($_record->has('relations')) {
1586 // TODO check if this needs to be done, as we might already deleting this "from the other side"
1587 $this->deleteLinkedRelations($_record);
1590 if ($_record->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
1591 Tinebase_FileSystem_RecordAttachments::getInstance()->deleteRecordAttachments($_record);
1596 * delete linked relations
1598 * @param Tinebase_Record_Interface $record
1599 * @param array $modelsToDelete
1600 * @param array $typesToDelete
1602 public function deleteLinkedRelations(Tinebase_Record_Interface $record, $modelsToDelete = array(), $typesToDelete = array())
1604 $relations = isset($record->relations) && $record->relations instanceof Tinebase_Record_RecordSet
1605 ? $record->relations
1606 : Tinebase_Relations::getInstance()->getRelations($this->_modelName, $this->_getBackendType(), $record->getId());
1608 if (count($relations) === 0) {
1612 // unset record relations
1613 Tinebase_Relations::getInstance()->setRelations($this->_modelName, $this->_getBackendType(), $record->getId(), array());
1615 if (empty($modelsToDelete)) {
1616 $modelsToDelete = $this->_relatedObjectsToDelete;
1618 if (empty($modelsToDelete) && empty($typesToDelete)) {
1622 // remove related objects
1623 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Deleting all '
1624 . implode(',', $modelsToDelete) . ' relations.');
1626 foreach ($relations as $relation) {
1627 if (in_array($relation->related_model, $modelsToDelete) || in_array($relation->type, $typesToDelete)) {
1628 list($appName, $i, $itemName) = explode('_', $relation->related_model);
1629 $appController = Tinebase_Core::getApplicationInstance($appName, $itemName);
1632 $appController->delete($relation->related_id);
1633 } catch (Exception $e) {
1634 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error deleting: ' . $e->getMessage());
1641 * check grant for action (CRUD)
1643 * @param Tinebase_Record_Interface $_record
1644 * @param string $_action
1645 * @param boolean $_throw
1646 * @param string $_errorMessage
1647 * @param Tinebase_Record_Interface $_oldRecord
1649 * @throws Tinebase_Exception_AccessDenied
1651 * @todo use this function in other create + update functions
1652 * @todo invent concept for simple adding of grants (plugins?)
1654 protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
1656 if ( ! $this->_doContainerACLChecks
1657 || ! $_record->has('container_id')) {
1661 if (! is_object(Tinebase_Core::getUser())) {
1662 throw new Tinebase_Exception_AccessDenied('User object required to check grants');
1665 // admin grant includes all others
1666 if (Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
1674 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_READ);
1677 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
1680 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_EDIT);
1683 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
1684 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_DELETE);
1689 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
1691 throw new Tinebase_Exception_AccessDenied($_errorMessage);
1699 * overwrite this function to check rights
1701 * @param string $_action {get|create|update|delete}
1703 * @throws Tinebase_Exception_AccessDenied
1705 protected function _checkRight($_action)
1711 * Removes containers where current user has no access to
1713 * @param Tinebase_Model_Filter_FilterGroup $_filter
1714 * @param string $_action get|update
1716 public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
1718 if (! $this->_doContainerACLChecks) {
1719 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1720 . ' Container ACL disabled for ' . $_filter->getModelName() . '.');
1724 $aclFilters = $_filter->getAclFilters();
1726 if (! $aclFilters) {
1727 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1728 . ' Force a standard containerFilter (specialNode = all) as ACL filter.');
1730 $containerFilter = $_filter->createFilter('container_id', 'specialNode', 'all', array('applicationName' => $_filter->getApplicationName()));
1731 $_filter->addFilter($containerFilter);
1734 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1735 . ' Setting filter grants for action ' . $_action);
1738 $_filter->setRequiredGrants(array(
1739 Tinebase_Model_Grants::GRANT_READ,
1740 Tinebase_Model_Grants::GRANT_ADMIN,
1744 $_filter->setRequiredGrants(array(
1745 Tinebase_Model_Grants::GRANT_EDIT,
1746 Tinebase_Model_Grants::GRANT_ADMIN,
1750 $_filter->setRequiredGrants(array(
1751 Tinebase_Model_Grants::GRANT_EXPORT,
1752 Tinebase_Model_Grants::GRANT_ADMIN,
1756 $_filter->setRequiredGrants(array(
1757 Tinebase_Model_Grants::GRANT_SYNC,
1758 Tinebase_Model_Grants::GRANT_ADMIN,
1762 throw new Tinebase_Exception_UnexpectedValue('Unknown action: ' . $_action);
1767 * saves alarms of given record
1769 * @param Tinebase_Record_Abstract $_record
1772 protected function _saveAlarms(Tinebase_Record_Abstract $_record)
1774 if (! $_record->alarms instanceof Tinebase_Record_RecordSet) {
1775 $_record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1777 $alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1779 // create / update alarms
1780 foreach ($_record->alarms as $alarm) {
1782 $this->_inspectAlarmSet($_record, $alarm);
1783 $alarms->addRecord($alarm);
1784 } catch (Tinebase_Exception_InvalidArgument $teia) {
1785 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $teia->getMessage());
1789 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1790 . " About to save " . count($alarms) . " alarms for {$_record->id} ");
1791 $_record->alarms = $alarms;
1793 Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_record);
1797 * inspect alarm and set time
1799 * @param Tinebase_Record_Abstract $_record
1800 * @param Tinebase_Model_Alarm $_alarm
1802 * @throws Tinebase_Exception_InvalidArgument
1804 protected function _inspectAlarmSet(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm)
1806 if (! $_record->{$this->_recordAlarmField} instanceof DateTime) {
1807 throw new Tinebase_Exception_InvalidArgument('alarm reference time is not set');
1810 $_alarm->setTime($_record->{$this->_recordAlarmField});
1814 * get and resolve all alarms of given record(s)
1816 * @param Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
1818 public function getAlarms($_record)
1820 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Resolving alarms and add them to record set.");
1822 $records = $_record instanceof Tinebase_Record_RecordSet ? $_record : new Tinebase_Record_RecordSet($this->_modelName, array($_record));
1824 $alarms = Tinebase_Alarm::getInstance()->getAlarmsOfRecord($this->_modelName, $records);
1826 foreach ($alarms as $alarm) {
1827 $record = $records->getById($alarm->record_id);
1829 if (!isset($record->alarms)) {
1830 $record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1833 if (!$record->alarms->getById($alarm->getId())) {
1834 $record->alarms->addRecord($alarm);
1838 foreach ($records as $record) {
1839 if (!isset($record->alarms)) {
1840 $record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1842 // calc minutes_before
1843 if ($record->has($this->_recordAlarmField) && $record->{$this->_recordAlarmField} instanceof DateTime) {
1844 $this->_inspectAlarmGet($record);
1851 * inspect alarms of record (all alarms minutes_before fields are set here by default)
1853 * @param Tinebase_Record_Abstract $_record
1856 protected function _inspectAlarmGet(Tinebase_Record_Abstract $_record)
1858 $_record->alarms->setMinutesBefore($_record->{$this->_recordAlarmField});
1862 * delete alarms for records
1864 * @param array $_recordIds
1866 protected function _deleteAlarmsForIds($_recordIds)
1868 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1869 . " Deleting alarms for records " . print_r($_recordIds, TRUE)
1872 Tinebase_Alarm::getInstance()->deleteAlarmsOfRecord($this->_modelName, $_recordIds);
1876 * convert input to recordset
1878 * input can have the following datatypes:
1879 * - Tinebase_Model_Filter_FilterGroup
1880 * - Tinebase_Record_RecordSet
1881 * - Tinebase_Record_Abstract
1882 * - string (single id)
1883 * - array (multiple ids)
1885 * @param mixed $_mixed
1886 * @param boolean $_refresh if this is TRUE, refresh the recordset by calling getMultiple
1887 * @param Tinebase_Model_Pagination $_pagination (only valid if $_mixed instanceof Tinebase_Model_Filter_FilterGroup)
1888 * @return Tinebase_Record_RecordSet
1890 protected function _convertToRecordSet($_mixed, $_refresh = FALSE, Tinebase_Model_Pagination $_pagination = NULL)
1892 if ($_mixed instanceof Tinebase_Model_Filter_FilterGroup) {
1893 // FILTER (Tinebase_Model_Filter_FilterGroup)
1894 $result = $this->search($_mixed, $_pagination);
1895 } elseif ($_mixed instanceof Tinebase_Record_RecordSet) {
1896 // RECORDSET (Tinebase_Record_RecordSet)
1897 $result = ($_refresh) ? $this->_backend->getMultiple($_mixed->getArrayOfIds()) : $_mixed;
1898 } elseif ($_mixed instanceof Tinebase_Record_Abstract) {
1899 // RECORD (Tinebase_Record_Abstract)
1901 $result = $this->_backend->getMultiple($_mixed->getId());
1903 $result = new Tinebase_Record_RecordSet(get_class($_mixed), array($_mixed));
1905 } elseif (is_string($_mixed) || is_array($_mixed)) {
1906 // SINGLE ID or ARRAY OF IDS
1907 $result = $this->_backend->getMultiple($_mixed);
1909 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1910 . ' Could not convert input param to RecordSet: Unsupported type: ' . gettype($_mixed));
1911 $result = new Tinebase_Record_RecordSet($this->_modelName);
1919 * creates dependent records after creating the parent record
1921 * @param Tinebase_Record_Interface $_createdRecord
1922 * @param Tinebase_Record_Interface $_record
1923 * @param string $_property
1924 * @param array $_fieldConfig
1926 protected function _createDependentRecords(Tinebase_Record_Interface $_createdRecord, Tinebase_Record_Interface $_record, $_property, $_fieldConfig)
1928 if (! (isset($_fieldConfig['dependentRecords']) || array_key_exists('dependentRecords', $_fieldConfig)) || ! $_fieldConfig['dependentRecords']) {
1932 if ($_record->has($_property) && $_record->{$_property}) {
1933 $recordClassName = $_fieldConfig['recordClassName'];
1934 $new = new Tinebase_Record_RecordSet($recordClassName);
1935 $ccn = $_fieldConfig['controllerClassName'];
1936 $controller = $ccn::getInstance();
1938 // legacy - should be already done in frontend json - remove if all record properties are record sets before getting to controller
1939 if (is_array($_record->{$_property})) {
1940 $rs = new Tinebase_Record_RecordSet($recordClassName);
1941 foreach ($_record->{$_property} as $recordArray) {
1942 $rec = new $recordClassName(array(),true);
1943 $rec->setFromJsonInUsersTimezone($recordArray);
1945 if (strlen($rec->getId()) < 40) {
1946 $rec->{$rec->getIdProperty()} = Tinebase_Record_Abstract::generateUID();
1949 $rs->addRecord($rec);
1951 $_record->{$_property} = $rs;
1953 foreach ($_record->{$_property} as $rec) {
1954 if (strlen($rec->getId()) < 40) {
1955 $rec->{$rec->getIdProperty()} = Tinebase_Record_Abstract::generateUID();
1961 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1962 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating ' . $_record->{$_property}->count() . ' dependent records on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
1965 foreach ($_record->{$_property} as $record) {
1966 $record->{$_fieldConfig['refIdField']} = $_createdRecord->getId();
1967 if (! $record->getId() || strlen($record->getId()) != 40) {
1968 $record->{$record->getIdProperty()} = NULL;
1970 $new->addRecord($controller->create($record));
1973 $_createdRecord->{$_property} = $new->toArray();
1978 * updates dependent records on update the parent record
1980 * @param Tinebase_Record_Interface $_record
1981 * @param Tinebase_Record_Interface $_oldRecord
1982 * @param string $_property
1983 * @param array $_fieldConfig
1985 protected function _updateDependentRecords(Tinebase_Record_Interface $_record, Tinebase_Record_Interface $_oldRecord, $_property, $_fieldConfig)
1987 if (! (isset($_fieldConfig['dependentRecords']) || array_key_exists('dependentRecords', $_fieldConfig)) || ! $_fieldConfig['dependentRecords']) {
1991 if (! isset ($_fieldConfig['refIdField'])) {
1992 throw new Tinebase_Exception_Record_DefinitionFailure('If a record is dependent, a refIdField has to be defined!');
1995 // don't handle dependent records on property if it is set to null or doesn't exist.
1996 if (($_record->{$_property} === NULL) || (! $_record->has($_property))) {
1997 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1998 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Skip updating dependent records (got NULL) on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName . ' with id = "' . $_record->getId() . '"');
2003 $ccn = $_fieldConfig['controllerClassName'];
2004 $controller = $ccn::getInstance();
2005 $recordClassName = $_fieldConfig['recordClassName'];
2006 $filterClassName = $_fieldConfig['filterClassName'];
2007 $existing = new Tinebase_Record_RecordSet($recordClassName);
2009 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2010 . ' ' . print_r($_record->{$_property}, TRUE));
2012 if (! empty($_record->{$_property}) && $_record->{$_property}) {
2014 // legacy - should be already done in frontend json - remove if all record properties are record sets before getting to controller
2015 if (is_array($_record->{$_property})) {
2016 $rs = new Tinebase_Record_RecordSet($recordClassName);
2017 foreach ($_record->{$_property} as $recordArray) {
2018 $rec = new $recordClassName(array(),true);
2019 $rec->setFromJsonInUsersTimezone($recordArray);
2020 $rs->addRecord($rec);
2022 $_record->{$_property} = $rs;
2025 $idProperty = $_record->{$_property}->getFirstRecord()->getIdProperty();
2028 $oldFilter = new $filterClassName(array(array('field' => $idProperty, 'operator' => 'in', 'value' => $_record->{$_property}->getId())));
2029 $oldRecords = $controller->search($oldFilter);
2031 foreach ($_record->{$_property} as $record) {
2033 $record->{$_fieldConfig['refIdField']} = $_record->getId();
2035 // update record if ID exists and has a length of 40 (it has a length of 10 if it is a timestamp)
2036 if ($record->getId() && strlen($record->getId()) == 40) {
2038 // do not try to update if the record hasn't changed
2039 $oldRecord = $oldRecords->getById($record->getId());
2041 if ($oldRecord && ! empty($oldRecord->diff($record)->diff)) {
2042 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2043 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Updating dependent record with id = "' . $record->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2045 $existing->addRecord($controller->update($record));
2047 $existing->addRecord($record);
2049 // create if is not existing already
2051 // try to find if it already exists (with corrupted id)
2052 if ($record->getId() == NULL) {
2053 $crc = $controller->create($record);
2054 $existing->addRecord($crc);
2056 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2057 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating dependent record with id = "' . $crc->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2063 $prevRecord = $controller->get($record->getId());
2065 if (! empty($prevRecord->diff($record)->diff)) {
2066 $existing->addRecord($controller->update($record));
2068 $existing->addRecord($record);
2071 } catch (Tinebase_Exception_NotFound $e) {
2073 $crc = $controller->create($record);
2074 $existing->addRecord($crc);
2076 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2077 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating dependent record with id = "' . $crc->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2085 $filter = new $filterClassName(isset($_fieldConfig['addFilters']) ? $_fieldConfig['addFilters'] : array(), 'AND');
2086 $filter->addFilter(new Tinebase_Model_Filter_Text($_fieldConfig['refIdField'], 'equals', $_record->getId()));
2088 // an empty array will remove all records on this property
2089 if (! empty($_record->{$_property})) {
2090 $filter->addFilter(new Tinebase_Model_Filter_Id('id', 'notin', $existing->getId()));
2093 $deleteIds = $controller->search($filter, NULL, FALSE, TRUE);
2095 if (! empty($deleteIds)) {
2096 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2097 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Deleting dependent records with id = "' . print_r($deleteIds, 1) . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2099 $controller->delete($deleteIds);
2101 $_record->{$_property} = $existing->toArray();
2104 protected function _createDependentRecord($record, $_record, $_fieldConfig, $controller, $recordSet)