Back to index

d-push  2.0
statemanager.php
Go to the documentation of this file.
00001 <?php
00002 /***********************************************
00003 * File      :   statemanager.php
00004 * Project   :   Z-Push
00005 * Descr     :   The StateManager uses a IStateMachine
00006 *               implementation to save data.
00007 *               SyncKey's are of the form {UUID}N, in
00008 *               which UUID is allocated during the
00009 *               first sync, and N is incremented
00010 *               for each request to 'GetNewSyncKey()'.
00011 *               A sync state is simple an opaque
00012 *               string value that can differ
00013 *               for each backend used - normally
00014 *               a list of items as the backend has
00015 *               sent them to the PIM. The backend
00016 *               can then use this backend
00017 *               information to compute the increments
00018 *               with current data.
00019 *               See FileStateMachine and IStateMachine
00020 *               for additional information.
00021 *
00022 * Created   :   26.12.2011
00023 *
00024 * Copyright 2007 - 2011 Zarafa Deutschland GmbH
00025 *
00026 * This program is free software: you can redistribute it and/or modify
00027 * it under the terms of the GNU Affero General Public License, version 3,
00028 * as published by the Free Software Foundation with the following additional
00029 * term according to sec. 7:
00030 *
00031 * According to sec. 7 of the GNU Affero General Public License, version 3,
00032 * the terms of the AGPL are supplemented with the following terms:
00033 *
00034 * "Zarafa" is a registered trademark of Zarafa B.V.
00035 * "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
00036 * The licensing of the Program under the AGPL does not imply a trademark license.
00037 * Therefore any rights, title and interest in our trademarks remain entirely with us.
00038 *
00039 * However, if you propagate an unmodified version of the Program you are
00040 * allowed to use the term "Z-Push" to indicate that you distribute the Program.
00041 * Furthermore you may use our trademarks where it is necessary to indicate
00042 * the intended purpose of a product or service provided you use it in accordance
00043 * with honest practices in industrial or commercial matters.
00044 * If you want to propagate modified versions of the Program under the name "Z-Push",
00045 * you may only do so if you have a written permission by Zarafa Deutschland GmbH
00046 * (to acquire a permission please contact Zarafa at trademark@zarafa.com).
00047 *
00048 * This program is distributed in the hope that it will be useful,
00049 * but WITHOUT ANY WARRANTY; without even the implied warranty of
00050 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00051 * GNU Affero General Public License for more details.
00052 *
00053 * You should have received a copy of the GNU Affero General Public License
00054 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00055 *
00056 * Consult LICENSE file for details
00057 ************************************************/
00058 
00059 class StateManager {
00060     const FIXEDHIERARCHYCOUNTER = 99999;
00061 
00062     // backend storage types
00063     const BACKENDSTORAGE_PERMANENT = 1;
00064     const BACKENDSTORAGE_STATE = 2;
00065 
00066     private $statemachine;
00067     private $device;
00068     private $hierarchyOperation = false;
00069     private $deleteOldStates = false;
00070 
00071     private $foldertype;
00072     private $uuid;
00073     private $oldStateCounter;
00074     private $newStateCounter;
00075     private $synchedFolders;
00076 
00077 
00083     public function StateManager() {
00084         $this->statemachine = ZPush::GetStateMachine();
00085         $this->hierarchyOperation = ZPush::HierarchyCommand(Request::GetCommandCode());
00086         $this->deleteOldStates = (Request::GetCommandCode() === ZPush::COMMAND_SYNC || $this->hierarchyOperation);
00087         $this->synchedFolders = array();
00088     }
00089 
00098     public function SetDevice(&$device) {
00099         $this->device = $device;
00100         return true;
00101     }
00102 
00109     public function GetSynchedFolders() {
00110         $synched = array();
00111         foreach ($this->device->GetAllFolderIds() as $folderid) {
00112             $uuid = $this->device->GetFolderUUID($folderid);
00113             if ($uuid)
00114                 $synched[] = $folderid;
00115         }
00116         return $synched;
00117     }
00118 
00127     public function GetSynchedFolderState($folderid) {
00128         // new SyncParameters are cached
00129         if (isset($this->synchedFolders[$folderid]))
00130             return $this->synchedFolders[$folderid];
00131 
00132         $uuid = $this->device->GetFolderUUID($folderid);
00133         if ($uuid) {
00134             try {
00135                 $data = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $uuid);
00136                 if ($data !== false) {
00137                     $this->synchedFolders[$folderid] = $data;
00138                 }
00139             }
00140             catch (StateNotFoundException $ex) { }
00141         }
00142 
00143         if (!isset($this->synchedFolders[$folderid]))
00144             $this->synchedFolders[$folderid] = new SyncParameters();
00145 
00146         return $this->synchedFolders[$folderid];
00147     }
00148 
00157     public function SetSynchedFolderState($spa) {
00158         // make sure the current uuid is linked on the device for the folder.
00159         // if not, old states will be automatically removed and the new ones linked
00160         self::LinkState($this->device, $spa->GetUuid(), $spa->GetFolderId());
00161 
00162         $spa->SetReferencePolicyKey($this->device->GetPolicyKey());
00163 
00164         return $this->statemachine->SetState($spa, $this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $spa->GetUuid());
00165     }
00166 
00176     function GetNewSyncKey($synckey) {
00177         if(!isset($synckey) || $synckey == "0" || $synckey == false) {
00178             $this->uuid = $this->getNewUuid();
00179             $this->newStateCounter = 1;
00180         }
00181         else {
00182             list($uuid, $counter) = self::ParseStateKey($synckey);
00183             $this->uuid = $uuid;
00184             $this->newStateCounter = $counter + 1;
00185         }
00186 
00187         return self::BuildStateKey($this->uuid, $this->newStateCounter);
00188     }
00189 
00199     public function GetSyncState($synckey) {
00200         // No sync state for sync key '0'
00201         if($synckey == "0") {
00202             $this->oldStateCounter = 0;
00203             return "";
00204         }
00205 
00206         // Check if synckey is allowed and set uuid and counter
00207         list($this->uuid, $this->oldStateCounter) = self::ParseStateKey($synckey);
00208 
00209         // make sure the hierarchy cache is in place
00210         if ($this->hierarchyOperation)
00211             $this->loadHierarchyCache();
00212 
00213         // the state machine will discard any sync states before this one, as they are no longer required
00214         return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
00215     }
00216 
00228     public function SetSyncState($synckey, $syncstate, $folderid = false) {
00229         $internalkey = self::BuildStateKey($this->uuid, $this->newStateCounter);
00230         if ($this->oldStateCounter != 0 && $synckey != $internalkey)
00231             throw new StateInvalidException(sprintf("Unexpected synckey value oldcounter: '%s' synckey: '%s' internal key: '%s'", $this->oldStateCounter, $synckey, $internalkey));
00232 
00233         // make sure the hierarchy cache is also saved
00234         if ($this->hierarchyOperation)
00235             $this->saveHierarchyCache();
00236 
00237         // announce this uuid to the device, while old uuid/states should be deleted
00238         self::LinkState($this->device, $this->uuid, $folderid);
00239 
00240         return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->newStateCounter);
00241     }
00242 
00249     public function GetSyncFailState() {
00250         if (!$this->uuid)
00251             return false;
00252 
00253         try {
00254             return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
00255         }
00256         catch (StateNotFoundException $snfex) {
00257             return false;
00258         }
00259     }
00260 
00269     public function SetSyncFailState($syncstate) {
00270         if ($this->oldStateCounter == 0)
00271             return false;
00272 
00273         return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter);
00274     }
00275 
00285     public function GetBackendStorage($type = self::BACKENDSTORAGE_PERMANENT) {
00286         if ($type == self::BACKENDSTORAGE_STATE) {
00287             if (!$this->uuid)
00288                 throw new StateNotYetAvailableException();
00289 
00290             return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
00291         }
00292         else {
00293             return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime());
00294         }
00295     }
00296 
00307     public function SetBackendStorage($data, $type = self::BACKENDSTORAGE_PERMANENT) {
00308         if ($type == self::BACKENDSTORAGE_STATE) {
00309         if (!$this->uuid)
00310             throw new StateNotYetAvailableException();
00311 
00312             // TODO serialization should be done in the StateMachine
00313             return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->newStateCounter);
00314         }
00315         else {
00316             return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime());
00317         }
00318     }
00319 
00331     public function InitializeFolderCache($folders) {
00332         if (!is_array($folders))
00333             return false;
00334 
00335         if (!isset($this->device))
00336             throw new FatalException("ASDevice not initialized");
00337 
00338         // redeclare this operation as hierarchyOperation
00339         $this->hierarchyOperation = true;
00340 
00341         // as there is no hierarchy uuid, we have to create one
00342         $this->uuid = $this->getNewUuid();
00343         $this->newStateCounter = self::FIXEDHIERARCHYCOUNTER;
00344 
00345         // initialize legacy HierarchCache
00346         $this->device->SetHierarchyCache($folders);
00347 
00348         // force saving the hierarchy cache!
00349         return $this->saveHierarchyCache(true);
00350     }
00351 
00352 
00369     static public function LinkState(&$device, $newUuid, $folderid = false) {
00370         $savedUuid = $device->GetFolderUUID($folderid);
00371         // delete 'old' states!
00372         if ($savedUuid != $newUuid) {
00373             // remove states but no need to notify device
00374             self::UnLinkState($device, $folderid, false);
00375 
00376             ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::linkState(#ASDevice, '%s','%s'): linked to uuid '%s'.", $newUuid, (($folderid === false)?'HierarchyCache':$folderid), $newUuid));
00377             return $device->SetFolderUUID($newUuid, $folderid);
00378         }
00379         return true;
00380     }
00381 
00397     static public function UnLinkState(&$device, $folderid, $removeFromDevice = true, $retrieveUUIDFromDevice = true) {
00398         if ($retrieveUUIDFromDevice === true)
00399             $savedUuid = $device->GetFolderUUID($folderid);
00400         else
00401             $savedUuid = $retrieveUUIDFromDevice;
00402 
00403         if ($savedUuid) {
00404             ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::UnLinkState('%s'): saved state '%s' will be deleted.", $folderid, $savedUuid));
00405             ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::DEFTYPE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
00406             ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FOLDERDATA, $savedUuid); // CPO
00407             ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FAILSAVE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
00408             ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
00409 
00410             // remove all messages which could not be synched before
00411             $device->RemoveIgnoredMessage($folderid, false);
00412 
00413             if ($folderid === false && $savedUuid !== false)
00414                 ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::HIERARCHY, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
00415         }
00416         // delete this id from the uuid cache
00417         if ($removeFromDevice)
00418             return $device->SetFolderUUID(false, $folderid);
00419         else
00420             return true;
00421     }
00422 
00432     static public function ParseStateKey($synckey) {
00433         $matches = array();
00434         if(!preg_match('/^\{([0-9A-Za-z-]+)\}([0-9]+)$/', $synckey, $matches))
00435             throw new StateInvalidException(sprintf("SyncKey '%s' is invalid", $synckey));
00436 
00437         return array($matches[1], (int)$matches[2]);
00438     }
00439 
00450     static public function BuildStateKey($uuid, $counter) {
00451         if(!preg_match('/^([0-9A-Za-z-]+)$/', $uuid, $matches))
00452             throw new StateInvalidException(sprintf("UUID '%s' is invalid", $uuid));
00453 
00454         return "{" . $uuid . "}" . $counter;
00455     }
00456 
00457 
00470     private function loadHierarchyCache() {
00471         if (!$this->hierarchyOperation)
00472             return false;
00473 
00474         ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager->loadHierarchyCache(): '%s-%s-%s-%d'", $this->device->GetDeviceId(), $this->uuid, IStateMachine::HIERARCHY, $this->oldStateCounter));
00475 
00476         // check if a full hierarchy sync might be necessary
00477         if ($this->device->GetFolderUUID(false) === false) {
00478             self::UnLinkState($this->device, false, false, $this->uuid);
00479             throw new StateNotFoundException("No hierarchy UUID linked to device. Requesting folder resync.");
00480         }
00481 
00482         $hierarchydata = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid , $this->oldStateCounter, $this->deleteOldStates);
00483         $this->device->SetHierarchyCache($hierarchydata);
00484         return true;
00485     }
00486 
00497     private function saveHierarchyCache($forceSaving = false) {
00498         if (!$this->hierarchyOperation && !$forceSaving)
00499             return false;
00500 
00501         // link the hierarchy cache again, if the UUID does not match the UUID saved in the devicedata
00502         if (($this->uuid != $this->device->GetFolderUUID() || $forceSaving) )
00503             self::LinkState($this->device, $this->uuid);
00504 
00505         // check all folders and deleted folders to update data of ASDevice and delete old states
00506         $hc = $this->device->getHierarchyCache();
00507         foreach ($hc->GetDeletedFolders() as $delfolder)
00508             self::UnLinkState($this->device, $delfolder->serverid);
00509 
00510         foreach ($hc->ExportFolders() as $folder)
00511             $this->device->SetFolderType($folder->serverid, $folder->type);
00512 
00513         return $this->statemachine->SetState($this->device->GetHierarchyCacheData(), $this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid, $this->newStateCounter);
00514     }
00515 
00522     private function getNewUuid() {
00523         return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
00524                     mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
00525                     mt_rand( 0, 0x0fff ) | 0x4000,
00526                     mt_rand( 0, 0x3fff ) | 0x8000,
00527                     mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) );
00528     }
00529 }
00530 ?>