source: trunk/zpush/lib/core/statemanager.php @ 7589

Revision 7589, 19.6 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
1<?php
2/***********************************************
3* File      :   statemanager.php
4* Project   :   Z-Push
5* Descr     :   The StateManager uses a IStateMachine
6*               implementation to save data.
7*               SyncKey's are of the form {UUID}N, in
8*               which UUID is allocated during the
9*               first sync, and N is incremented
10*               for each request to 'GetNewSyncKey()'.
11*               A sync state is simple an opaque
12*               string value that can differ
13*               for each backend used - normally
14*               a list of items as the backend has
15*               sent them to the PIM. The backend
16*               can then use this backend
17*               information to compute the increments
18*               with current data.
19*               See FileStateMachine and IStateMachine
20*               for additional information.
21*
22* Created   :   26.12.2011
23*
24* Copyright 2007 - 2012 Zarafa Deutschland GmbH
25*
26* This program is free software: you can redistribute it and/or modify
27* it under the terms of the GNU Affero General Public License, version 3,
28* as published by the Free Software Foundation with the following additional
29* term according to sec. 7:
30*
31* According to sec. 7 of the GNU Affero General Public License, version 3,
32* the terms of the AGPL are supplemented with the following terms:
33*
34* "Zarafa" is a registered trademark of Zarafa B.V.
35* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
36* The licensing of the Program under the AGPL does not imply a trademark license.
37* Therefore any rights, title and interest in our trademarks remain entirely with us.
38*
39* However, if you propagate an unmodified version of the Program you are
40* allowed to use the term "Z-Push" to indicate that you distribute the Program.
41* Furthermore you may use our trademarks where it is necessary to indicate
42* the intended purpose of a product or service provided you use it in accordance
43* with honest practices in industrial or commercial matters.
44* If you want to propagate modified versions of the Program under the name "Z-Push",
45* you may only do so if you have a written permission by Zarafa Deutschland GmbH
46* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
47*
48* This program is distributed in the hope that it will be useful,
49* but WITHOUT ANY WARRANTY; without even the implied warranty of
50* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51* GNU Affero General Public License for more details.
52*
53* You should have received a copy of the GNU Affero General Public License
54* along with this program.  If not, see <http://www.gnu.org/licenses/>.
55*
56* Consult LICENSE file for details
57************************************************/
58
59class StateManager {
60    const FIXEDHIERARCHYCOUNTER = 99999;
61
62    // backend storage types
63    const BACKENDSTORAGE_PERMANENT = 1;
64    const BACKENDSTORAGE_STATE = 2;
65
66    private $statemachine;
67    private $device;
68    private $hierarchyOperation = false;
69    private $deleteOldStates = false;
70
71    private $foldertype;
72    private $uuid;
73    private $oldStateCounter;
74    private $newStateCounter;
75    private $synchedFolders;
76
77
78    /**
79     * Constructor
80     *
81     * @access public
82     */
83    public function StateManager() {
84        $this->statemachine = ZPush::GetStateMachine();
85        $this->hierarchyOperation = ZPush::HierarchyCommand(Request::GetCommandCode());
86        $this->deleteOldStates = (Request::GetCommandCode() === ZPush::COMMAND_SYNC || $this->hierarchyOperation);
87        $this->synchedFolders = array();
88    }
89
90    /**
91     * Sets an ASDevice for the Statemanager to work with
92     *
93     * @param ASDevice  $device
94     *
95     * @access public
96     * @return boolean
97     */
98    public function SetDevice(&$device) {
99        $this->device = $device;
100        return true;
101    }
102
103    /**
104     * Returns an array will all synchronized folderids
105     *
106     * @access public
107     * @return array
108     */
109    public function GetSynchedFolders() {
110        $synched = array();
111        foreach ($this->device->GetAllFolderIds() as $folderid) {
112            $uuid = $this->device->GetFolderUUID($folderid);
113            if ($uuid)
114                $synched[] = $folderid;
115        }
116        return $synched;
117    }
118
119    /**
120     * Returns a folder state (SyncParameters) for a folder id
121     *
122     * @param $folderid
123     *
124     * @access public
125     * @return SyncParameters
126     */
127    public function GetSynchedFolderState($folderid) {
128        // new SyncParameters are cached
129        if (isset($this->synchedFolders[$folderid]))
130            return $this->synchedFolders[$folderid];
131
132        $uuid = $this->device->GetFolderUUID($folderid);
133        if ($uuid) {
134            try {
135                $data = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $uuid);
136                if ($data !== false) {
137                    $this->synchedFolders[$folderid] = $data;
138                }
139            }
140            catch (StateNotFoundException $ex) { }
141        }
142
143        if (!isset($this->synchedFolders[$folderid]))
144            $this->synchedFolders[$folderid] = new SyncParameters();
145
146        return $this->synchedFolders[$folderid];
147    }
148
149    /**
150     * Saves a folder state - SyncParameters object
151     *
152     * @param SyncParamerters    $spa
153     *
154     * @access public
155     * @return boolean
156     */
157    public function SetSynchedFolderState($spa) {
158        // make sure the current uuid is linked on the device for the folder.
159        // if not, old states will be automatically removed and the new ones linked
160        self::LinkState($this->device, $spa->GetUuid(), $spa->GetFolderId());
161
162        $spa->SetReferencePolicyKey($this->device->GetPolicyKey());
163
164        return $this->statemachine->SetState($spa, $this->device->GetDeviceId(), IStateMachine::FOLDERDATA, $spa->GetUuid());
165    }
166
167    /**
168     * Gets the new sync key for a specified sync key. The new sync state must be
169     * associated to this sync key when calling SetSyncState()
170     *
171     * @param string    $synckey
172     *
173     * @access public
174     * @return string
175     */
176    function GetNewSyncKey($synckey) {
177        if(!isset($synckey) || $synckey == "0" || $synckey == false) {
178            $this->uuid = $this->getNewUuid();
179            $this->newStateCounter = 1;
180        }
181        else {
182            list($uuid, $counter) = self::ParseStateKey($synckey);
183            $this->uuid = $uuid;
184            $this->newStateCounter = $counter + 1;
185        }
186
187        return self::BuildStateKey($this->uuid, $this->newStateCounter);
188    }
189
190    /**
191     * Gets the state for a specified synckey (uuid + counter)
192     *
193     * @param string    $synckey
194     *
195     * @access public
196     * @return string
197     * @throws StateInvalidException, StateNotFoundException
198     */
199    public function GetSyncState($synckey) {
200        // No sync state for sync key '0'
201        if($synckey == "0") {
202            $this->oldStateCounter = 0;
203            return "";
204        }
205
206        // Check if synckey is allowed and set uuid and counter
207        list($this->uuid, $this->oldStateCounter) = self::ParseStateKey($synckey);
208
209        // make sure the hierarchy cache is in place
210        if ($this->hierarchyOperation)
211            $this->loadHierarchyCache();
212
213        // the state machine will discard any sync states before this one, as they are no longer required
214        return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
215    }
216
217    /**
218     * Writes the sync state to a new synckey
219     *
220     * @param string    $synckey
221     * @param string    $syncstate
222     * @param string    $folderid       (opt) the synckey is associated with the folder - should always be set when performing CONTENT operations
223     *
224     * @access public
225     * @return boolean
226     * @throws StateInvalidException
227     */
228    public function SetSyncState($synckey, $syncstate, $folderid = false) {
229        $internalkey = self::BuildStateKey($this->uuid, $this->newStateCounter);
230        if ($this->oldStateCounter != 0 && $synckey != $internalkey)
231            throw new StateInvalidException(sprintf("Unexpected synckey value oldcounter: '%s' synckey: '%s' internal key: '%s'", $this->oldStateCounter, $synckey, $internalkey));
232
233        // make sure the hierarchy cache is also saved
234        if ($this->hierarchyOperation)
235            $this->saveHierarchyCache();
236
237        // announce this uuid to the device, while old uuid/states should be deleted
238        self::LinkState($this->device, $this->uuid, $folderid);
239
240        return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::DEFTYPE, $this->uuid, $this->newStateCounter);
241    }
242
243    /**
244     * Gets the failsave sync state for the current synckey
245     *
246     * @access public
247     * @return array/boolean    false if not available
248     */
249    public function GetSyncFailState() {
250        if (!$this->uuid)
251            return false;
252
253        try {
254            return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
255        }
256        catch (StateNotFoundException $snfex) {
257            return false;
258        }
259    }
260
261    /**
262     * Writes the failsave sync state for the current (old) synckey
263     *
264     * @param mixed     $syncstate
265     *
266     * @access public
267     * @return boolean
268     */
269    public function SetSyncFailState($syncstate) {
270        if ($this->oldStateCounter == 0)
271            return false;
272
273        return $this->statemachine->SetState($syncstate, $this->device->GetDeviceId(), IStateMachine::FAILSAVE, $this->uuid, $this->oldStateCounter);
274    }
275
276    /**
277     * Gets the backendstorage data
278     *
279     * @param int   $type       permanent or state related storage
280     *
281     * @access public
282     * @return mixed
283     * @throws StateNotYetAvailableException, StateNotFoundException
284     */
285    public function GetBackendStorage($type = self::BACKENDSTORAGE_PERMANENT) {
286        if ($type == self::BACKENDSTORAGE_STATE) {
287            if (!$this->uuid)
288                throw new StateNotYetAvailableException();
289
290            return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->oldStateCounter, $this->deleteOldStates);
291        }
292        else {
293            return $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime());
294        }
295    }
296
297   /**
298     * Writes the backendstorage data
299     *
300     * @param mixed $data
301     * @param int   $type       permanent or state related storage
302     *
303     * @access public
304     * @return int              amount of bytes saved
305     * @throws StateNotYetAvailableException, StateNotFoundException
306     */
307    public function SetBackendStorage($data, $type = self::BACKENDSTORAGE_PERMANENT) {
308        if ($type == self::BACKENDSTORAGE_STATE) {
309        if (!$this->uuid)
310            throw new StateNotYetAvailableException();
311
312            // TODO serialization should be done in the StateMachine
313            return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $this->uuid, $this->newStateCounter);
314        }
315        else {
316            return $this->statemachine->SetState($data, $this->device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, false, $this->device->GetFirstSyncTime());
317        }
318    }
319
320    /**
321     * Initializes the HierarchyCache for legacy syncs
322     * this is for AS 1.0 compatibility:
323     * save folder information synched with GetHierarchy()
324     * handled by StateManager
325     *
326     * @param string    $folders            Array with folder information
327     *
328     * @access public
329     * @return boolean
330     */
331    public function InitializeFolderCache($folders) {
332        if (!is_array($folders))
333            return false;
334
335        if (!isset($this->device))
336            throw new FatalException("ASDevice not initialized");
337
338        // redeclare this operation as hierarchyOperation
339        $this->hierarchyOperation = true;
340
341        // as there is no hierarchy uuid, we have to create one
342        $this->uuid = $this->getNewUuid();
343        $this->newStateCounter = self::FIXEDHIERARCHYCOUNTER;
344
345        // initialize legacy HierarchCache
346        $this->device->SetHierarchyCache($folders);
347
348        // force saving the hierarchy cache!
349        return $this->saveHierarchyCache(true);
350    }
351
352
353    /**----------------------------------------------------------------------------------------------------------
354     * static StateManager methods
355     */
356
357    /**
358     * Links a folderid to the a UUID
359     * Old states are removed if an folderid is linked to a new UUID
360     * assisting the StateMachine to get rid of old data.
361     *
362     * @param ASDevice  $device
363     * @param string    $uuid               the uuid to link to
364     * @param string    $folderid           (opt) if not set, hierarchy state is linked
365     *
366     * @access public
367     * @return boolean
368     */
369    static public function LinkState(&$device, $newUuid, $folderid = false) {
370        $savedUuid = $device->GetFolderUUID($folderid);
371        // delete 'old' states!
372        if ($savedUuid != $newUuid) {
373            // remove states but no need to notify device
374            self::UnLinkState($device, $folderid, false);
375
376            ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::linkState(#ASDevice, '%s','%s'): linked to uuid '%s'.", $newUuid, (($folderid === false)?'HierarchyCache':$folderid), $newUuid));
377            return $device->SetFolderUUID($newUuid, $folderid);
378        }
379        return true;
380    }
381
382    /**
383     * UnLinks all states from a folder id
384     * Old states are removed assisting the StateMachine to get rid of old data.
385     * The UUID is then removed from the device
386     *
387     * @param ASDevice  $device
388     * @param string    $folderid
389     * @param boolean   $removeFromDevice       indicates if the device should be
390     *                                          notified that the state was removed
391     * @param boolean   $retrieveUUIDFromDevice indicates if the UUID should be retrieved from
392     *                                          device. If not true this parameter will be used as UUID.
393     *
394     * @access public
395     * @return boolean
396     */
397    static public function UnLinkState(&$device, $folderid, $removeFromDevice = true, $retrieveUUIDFromDevice = true) {
398        if ($retrieveUUIDFromDevice === true)
399            $savedUuid = $device->GetFolderUUID($folderid);
400        else
401            $savedUuid = $retrieveUUIDFromDevice;
402
403        if ($savedUuid) {
404            ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager::UnLinkState('%s'): saved state '%s' will be deleted.", $folderid, $savedUuid));
405            ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::DEFTYPE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
406            ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FOLDERDATA, $savedUuid); // CPO
407            ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::FAILSAVE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
408            ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::BACKENDSTORAGE, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
409
410            // remove all messages which could not be synched before
411            $device->RemoveIgnoredMessage($folderid, false);
412
413            if ($folderid === false && $savedUuid !== false)
414                ZPush::GetStateMachine()->CleanStates($device->GetDeviceId(), IStateMachine::HIERARCHY, $savedUuid, self::FIXEDHIERARCHYCOUNTER *2);
415        }
416        // delete this id from the uuid cache
417        if ($removeFromDevice)
418            return $device->SetFolderUUID(false, $folderid);
419        else
420            return true;
421    }
422
423    /**
424     * Parses a SyncKey and returns UUID and counter
425     *
426     * @param string    $synckey
427     *
428     * @access public
429     * @return array            uuid, counter
430     * @throws StateInvalidException
431     */
432    static public function ParseStateKey($synckey) {
433        $matches = array();
434        if(!preg_match('/^\{([0-9A-Za-z-]+)\}([0-9]+)$/', $synckey, $matches))
435            throw new StateInvalidException(sprintf("SyncKey '%s' is invalid", $synckey));
436
437        return array($matches[1], (int)$matches[2]);
438    }
439
440    /**
441     * Builds a SyncKey from a UUID and counter
442     *
443     * @param string    $uuid
444     * @param int       $counter
445     *
446     * @access public
447     * @return string           syncKey
448     * @throws StateInvalidException
449     */
450    static public function BuildStateKey($uuid, $counter) {
451        if(!preg_match('/^([0-9A-Za-z-]+)$/', $uuid, $matches))
452            throw new StateInvalidException(sprintf("UUID '%s' is invalid", $uuid));
453
454        return "{" . $uuid . "}" . $counter;
455    }
456
457
458    /**----------------------------------------------------------------------------------------------------------
459     * private StateManager methods
460     */
461
462    /**
463     * Loads the HierarchyCacheState and initializes the HierarchyChache
464     * if this is an hierarchy operation
465     *
466     * @access private
467     * @return boolean
468     * @throws StateNotFoundException
469     */
470    private function loadHierarchyCache() {
471        if (!$this->hierarchyOperation)
472            return false;
473
474        ZLog::Write(LOGLEVEL_DEBUG, sprintf("StateManager->loadHierarchyCache(): '%s-%s-%s-%d'", $this->device->GetDeviceId(), $this->uuid, IStateMachine::HIERARCHY, $this->oldStateCounter));
475
476        // check if a full hierarchy sync might be necessary
477        if ($this->device->GetFolderUUID(false) === false) {
478            self::UnLinkState($this->device, false, false, $this->uuid);
479            throw new StateNotFoundException("No hierarchy UUID linked to device. Requesting folder resync.");
480        }
481
482        $hierarchydata = $this->statemachine->GetState($this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid , $this->oldStateCounter, $this->deleteOldStates);
483        $this->device->SetHierarchyCache($hierarchydata);
484        return true;
485    }
486
487    /**
488     * Saves the HierarchyCacheState of the HierarchyChache
489     * if this is an hierarchy operation
490     *
491     * @param boolean   $forceLoad      indicates if the cache should be saved also if not a hierary operation
492     *
493     * @access private
494     * @return boolean
495     * @throws StateInvalidException
496     */
497    private function saveHierarchyCache($forceSaving = false) {
498        if (!$this->hierarchyOperation && !$forceSaving)
499            return false;
500
501        // link the hierarchy cache again, if the UUID does not match the UUID saved in the devicedata
502        if (($this->uuid != $this->device->GetFolderUUID() || $forceSaving) )
503            self::LinkState($this->device, $this->uuid);
504
505        // check all folders and deleted folders to update data of ASDevice and delete old states
506        $hc = $this->device->getHierarchyCache();
507        foreach ($hc->GetDeletedFolders() as $delfolder)
508            self::UnLinkState($this->device, $delfolder->serverid);
509
510        foreach ($hc->ExportFolders() as $folder)
511            $this->device->SetFolderType($folder->serverid, $folder->type);
512
513        return $this->statemachine->SetState($this->device->GetHierarchyCacheData(), $this->device->GetDeviceId(), IStateMachine::HIERARCHY, $this->uuid, $this->newStateCounter);
514    }
515
516    /**
517     * Generates a new UUID
518     *
519     * @access private
520     * @return string
521     */
522    private function getNewUuid() {
523        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
524                    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
525                    mt_rand( 0, 0x0fff ) | 0x4000,
526                    mt_rand( 0, 0x3fff ) | 0x8000,
527                    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) );
528    }
529}
530?>
Note: See TracBrowser for help on using the repository browser.