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

Revision 7589, 26.0 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      :   synccollections.php
4* Project   :   Z-Push
5* Descr     :   This is basically a list of synched folders with it's
6*               respective SyncParameters, while some additional parameters
7*               which are not stored there can be kept here.
8*               The class also provides CheckForChanges which is basically
9*               a loop through all collections checking for changes.
10*               SyncCollections is used for Sync (with and without heartbeat)
11*               and Ping connections.
12*               To check for changes in Heartbeat and Ping requeste the same
13*               sync states as for the default synchronization are used.
14*
15* Created   :   06.01.2012
16*
17* Copyright 2007 - 2012 Zarafa Deutschland GmbH
18*
19* This program is free software: you can redistribute it and/or modify
20* it under the terms of the GNU Affero General Public License, version 3,
21* as published by the Free Software Foundation with the following additional
22* term according to sec. 7:
23*
24* According to sec. 7 of the GNU Affero General Public License, version 3,
25* the terms of the AGPL are supplemented with the following terms:
26*
27* "Zarafa" is a registered trademark of Zarafa B.V.
28* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
29* The licensing of the Program under the AGPL does not imply a trademark license.
30* Therefore any rights, title and interest in our trademarks remain entirely with us.
31*
32* However, if you propagate an unmodified version of the Program you are
33* allowed to use the term "Z-Push" to indicate that you distribute the Program.
34* Furthermore you may use our trademarks where it is necessary to indicate
35* the intended purpose of a product or service provided you use it in accordance
36* with honest practices in industrial or commercial matters.
37* If you want to propagate modified versions of the Program under the name "Z-Push",
38* you may only do so if you have a written permission by Zarafa Deutschland GmbH
39* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
40*
41* This program is distributed in the hope that it will be useful,
42* but WITHOUT ANY WARRANTY; without even the implied warranty of
43* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44* GNU Affero General Public License for more details.
45*
46* You should have received a copy of the GNU Affero General Public License
47* along with this program.  If not, see <http://www.gnu.org/licenses/>.
48*
49* Consult LICENSE file for details
50************************************************/
51
52
53class SyncCollections implements Iterator {
54    const ERROR_NO_COLLECTIONS = 1;
55    const ERROR_WRONG_HIERARCHY = 2;
56
57    private $stateManager;
58
59    private $collections = array();
60    private $addparms = array();
61    private $changes = array();
62    private $saveData = true;
63
64    private $refPolicyKey = false;
65    private $refLifetime = false;
66
67    private $globalWindowSize;
68    private $lastSyncTime;
69
70    private $waitingTime = 0;
71
72
73    /**
74     * Constructor
75     */
76    public function SyncCollections() {
77    }
78
79    /**
80     * Sets the StateManager for this object
81     * If this is not done and a method needs it, the StateManager will be
82     * requested from the DeviceManager
83     *
84     * @param StateManager  $statemanager
85     *
86     * @access public
87     * @return
88     */
89    public function SetStateManager($statemanager) {
90        $this->stateManager = $statemanager;
91    }
92
93    /**
94     * Loads all collections known for the current device
95     *
96     * @param boolean $overwriteLoaded          (opt) overwrites Collection with saved state if set to true
97     * @param boolean $loadState                (opt) indicates if the collection sync state should be loaded, default true
98     * @param boolean $checkPermissions         (opt) if set to true each folder will pass
99     *                                          through a backend->Setup() to check permissions.
100     *                                          If this fails a StatusException will be thrown.
101     *
102     * @access public
103     * @throws StatusException                  with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails
104     * @throws StateNotFoundException           if the sync state can not be found ($loadState = true)
105     * @return boolean
106     */
107    public function LoadAllCollections($overwriteLoaded = false, $loadState = false, $checkPermissions = false) {
108        $this->loadStateManager();
109
110        $invalidStates = false;
111        foreach($this->stateManager->GetSynchedFolders() as $folderid) {
112            if ($overwriteLoaded === false && isset($this->collections[$folderid]))
113                continue;
114
115            // Load Collection!
116            if (! $this->LoadCollection($folderid, $loadState, $checkPermissions))
117                $invalidStates = true;
118        }
119
120        if ($invalidStates)
121            throw new StateInvalidException("Invalid states found while loading collections. Forcing sync");
122
123        return true;
124    }
125
126    /**
127     * Loads all collections known for the current device
128     *
129     * @param boolean $folderid                 folder id to be loaded
130     * @param boolean $loadState                (opt) indicates if the collection sync state should be loaded, default true
131     * @param boolean $checkPermissions         (opt) if set to true each folder will pass
132     *                                          through a backend->Setup() to check permissions.
133     *                                          If this fails a StatusException will be thrown.
134     *
135     * @access public
136     * @throws StatusException                  with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails
137     * @throws StateNotFoundException           if the sync state can not be found ($loadState = true)
138     * @return boolean
139     */
140    public function LoadCollection($folderid, $loadState = false, $checkPermissions = false) {
141        $this->loadStateManager();
142
143        try {
144            // Get SyncParameters for the folder from the state
145            $spa = $this->stateManager->GetSynchedFolderState($folderid);
146
147            // TODO remove resync of folders for < Z-Push 2 beta4 users
148            // this forces a resync of all states previous to Z-Push 2 beta4
149            if (! $spa instanceof SyncParameters)
150                throw new StateInvalidException("Saved state are not of type SyncParameters");
151        }
152        catch (StateInvalidException $sive) {
153            // in case there is something wrong with the state, just stop here
154            // later when trying to retrieve the SyncParameters nothing will be found
155
156            // we also generate a fake change, so a sync on this folder is triggered
157            $this->changes[$folderid] = 1;
158
159            return false;
160        }
161
162        // if this is an additional folder the backend has to be setup correctly
163        if ($checkPermissions === true && ! ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId())))
164            throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), self::ERROR_WRONG_HIERARCHY);
165
166        // add collection to object
167        $this->AddCollection($spa);
168
169        // load the latest known syncstate if requested
170        if ($loadState === true)
171            $this->addparms[$folderid]["state"] = $this->stateManager->GetSyncState($spa->GetLatestSyncKey());
172
173        return true;
174    }
175
176    /**
177     * Saves a SyncParameters Object
178     *
179     * @param SyncParamerts $spa
180     *
181     * @access public
182     * @return boolean
183     */
184    public function SaveCollection($spa) {
185        if (! $this->saveData)
186            return false;
187
188        if ($spa->IsDataChanged()) {
189            $this->loadStateManager();
190            ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->SaveCollection(): Data of folder '%s' changed", $spa->GetFolderId()));
191
192            // save new windowsize
193            if (isset($this->globalWindowSize))
194                $spa->SetWindowSize($this->globalWindowSize);
195
196            // update latest lifetime
197            if (isset($this->refLifetime))
198                $spa->SetReferenceLifetime($this->refLifetime);
199
200            return $this->stateManager->SetSynchedFolderState($spa);
201        }
202        return false;
203    }
204
205    /**
206     * Adds a SyncParameters object to the current list of collections
207     *
208     * @param SyncParameters $spa
209     *
210     * @access public
211     * @return boolean
212     */
213    public function AddCollection($spa) {
214        $this->collections[$spa->GetFolderId()] = $spa;
215
216        if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) {
217            $this->lastSyncTime = $spa->GetLastSyncTime();
218
219            // use SyncParameters PolicyKey as reference if available
220            if ($spa->HasReferencePolicyKey())
221                $this->refPolicyKey = $spa->GetReferencePolicyKey();
222
223            // use SyncParameters LifeTime as reference if available
224            if ($spa->HasReferenceLifetime())
225                $this->refLifetime = $spa->GetReferenceLifetime();
226        }
227
228        return true;
229    }
230
231    /**
232     * Returns a previousily added or loaded SyncParameters object for a folderid
233     *
234     * @param SyncParameters $spa
235     *
236     * @access public
237     * @return SyncParameters / boolean      false if no SyncParameters object is found for folderid
238     */
239    public function GetCollection($folderid) {
240        if (isset($this->collections[$folderid]))
241            return $this->collections[$folderid];
242        else
243            return false;
244    }
245
246    /**
247     * Indicates if there are any loaded CPOs
248     *
249     * @access public
250     * @return boolean
251     */
252    public function HasCollections() {
253        return ! empty($this->collections);
254    }
255
256    /**
257     * Add a non-permanent key/value pair for a SyncParameters object
258     *
259     * @param SyncParameters    $spa    target SyncParameters
260     * @param string            $key
261     * @param mixed             $value
262     *
263     * @access public
264     * @return boolean
265     */
266    public function AddParameter($spa, $key, $value) {
267        if (!$spa->HasFolderId())
268            return false;
269
270        $folderid = $spa->GetFolderId();
271        if (!isset($this->addparms[$folderid]))
272            $this->addparms[$folderid] = array();
273
274        $this->addparms[$folderid][$key] = $value;
275        return true;
276    }
277
278    /**
279     * Returns a previousily set non-permanent value for a SyncParameters object
280     *
281     * @param SyncParameters    $spa    target SyncParameters
282     * @param string            $key
283     *
284     * @access public
285     * @return mixed            returns 'null' if nothing set
286     */
287    public function GetParameter($spa, $key) {
288        if (isset($this->addparms[$spa->GetFolderId()]) && isset($this->addparms[$spa->GetFolderId()][$key]))
289            return $this->addparms[$spa->GetFolderId()][$key];
290        else
291            return null;
292    }
293
294    /**
295     * Returns the latest known PolicyKey to be used as reference
296     *
297     * @access public
298     * @return int/boolen       returns false if nothing found in collections
299     */
300    public function GetReferencePolicyKey() {
301        return $this->refPolicyKey;
302    }
303
304    /**
305     * Sets a global window size which should be used for all collections
306     * in a case of a heartbeat and/or partial sync
307     *
308     * @param int   $windowsize
309     *
310     * @access public
311     * @return boolean
312     */
313    public function SetGlobalWindowSize($windowsize) {
314        $this->globalWindowSize = $windowsize;
315        return true;
316    }
317
318    /**
319     * Returns the global window size which should be used for all collections
320     * in a case of a heartbeat and/or partial sync
321     *
322     * @access public
323     * @return int/boolean          returns false if not set or not available
324     */
325    public function GetGlobalWindowSize() {
326        if (!isset($this->globalWindowSize))
327            return false;
328
329        return $this->globalWindowSize;
330    }
331
332    /**
333     * Sets the lifetime for heartbeat or ping connections
334     *
335     * @param int   $lifetime       time in seconds
336     *
337     * @access public
338     * @return boolean
339     */
340    public function SetLifetime($lifetime) {
341        $this->refLifetime = $lifetime;
342        return true;
343    }
344
345    /**
346     * Sets the lifetime for heartbeat or ping connections
347     * previousily set or saved in a collection
348     *
349     * @access public
350     * @return int                  returns 600 as default if nothing set or not available
351     */
352    public function GetLifetime() {
353        if (!isset( $this->refLifetime) || $this->refLifetime === false)
354            return 600;
355
356        return $this->refLifetime;
357    }
358
359    /**
360     * Returns the timestamp of the last synchronization for all
361     * loaded collections
362     *
363     * @access public
364     * @return int                  timestamp
365     */
366    public function GetLastSyncTime() {
367        return $this->lastSyncTime;
368    }
369
370    /**
371     * Returns the timestamp of the last synchronization of a device.
372     *
373     * @param $device       an ASDevice
374     *
375     * @access public
376     * @return int                  timestamp
377     */
378    static public function GetLastSyncTimeOfDevice(&$device) {
379        // we need a StateManager for this operation
380        $stateManager = new StateManager();
381        $stateManager->SetDevice($device);
382
383        $sc = new SyncCollections();
384        $sc->SetStateManager($stateManager);
385
386        // load all collections of device without loading states or checking permissions
387        $sc->LoadAllCollections(true, false, false);
388
389        return $sc->GetLastSyncTime();
390    }
391
392    /**
393     * Checks if the currently known collections for changes for $lifetime seconds.
394     * If the backend provides a ChangesSink the sink will be used.
395     * If not every $interval seconds an exporter will be configured for each
396     * folder to perform GetChangeCount().
397     *
398     * @param int       $lifetime       (opt) total lifetime to wait for changes / default 600s
399     * @param int       $interval       (opt) time between blocking operations of sink or polling / default 30s
400     * @param boolean   $onlyPingable   (opt) only check for folders which have the PingableFlag
401     *
402     * @access public
403     * @return boolean              indicating if changes were found
404     * @throws StatusException      with code SyncCollections::ERROR_NO_COLLECTIONS if no collections available
405     *                              with code SyncCollections::ERROR_WRONG_HIERARCHY if there were errors getting changes
406     */
407    public function CheckForChanges($lifetime = 600, $interval = 30, $onlyPingable = false) {
408        $classes = array();
409        foreach ($this->collections as $folderid => $spa){
410            if ($onlyPingable && $spa->GetPingableFlag() !== true)
411                continue;
412
413            if (!isset($classes[$spa->GetContentClass()]))
414                $classes[$spa->GetContentClass()] = 0;
415            $classes[$spa->GetContentClass()] += 1;
416        }
417        if (empty($classes))
418            $checkClasses = "policies only";
419        else if (array_sum($classes) > 4) {
420            $checkClasses = "";
421            foreach($classes as $class=>$count) {
422                if ($count == 1)
423                    $checkClasses .= sprintf("%s ", $class);
424                else
425                    $checkClasses .= sprintf("%s(%d) ", $class, $count);
426            }
427        }
428        else
429            $checkClasses = implode(" ", array_keys($classes));
430
431        $pingTracking = new PingTracking();
432        $this->changes = array();
433        $changesAvailable = false;
434
435        ZPush::GetTopCollector()->SetAsPushConnection();
436        ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true);
437        ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes))?'policy':'store', $lifetime));
438
439        // use changes sink where available
440        $changesSink = false;
441        $forceRealExport = 0;
442        // do not create changessink if there are no folders
443        if (!empty($classes) && ZPush::GetBackend()->HasChangesSink()) {
444            $changesSink = true;
445
446            // initialize all possible folders
447            foreach ($this->collections as $folderid => $spa) {
448                if ($onlyPingable && $spa->GetPingableFlag() !== true)
449                    continue;
450
451                // switch user store if this is a additional folder and initialize sink
452                ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid));
453                if (! ZPush::GetBackend()->ChangesSinkInitialize($folderid))
454                    throw new StatusException(sprintf("Error initializing ChangesSink for folder id '%s'", $folderid), self::ERROR_WRONG_HIERARCHY);
455            }
456        }
457
458        // wait for changes
459        $started = time();
460        $endat = time() + $lifetime;
461        while(($now = time()) < $endat) {
462            // how long are we waiting for changes
463            $this->waitingTime = $now-$started;
464
465            $nextInterval = $interval;
466            // we should not block longer than the lifetime
467            if ($endat - $now < $nextInterval)
468                $nextInterval = $endat - $now;
469
470            // Check if provisioning is necessary
471            // if a PolicyKey was sent use it. If not, compare with the ReferencePolicyKey
472            if (PROVISIONING === true && $this->GetReferencePolicyKey() !== false && ZPush::GetDeviceManager()->ProvisioningRequired($this->GetReferencePolicyKey(), true))
473                // the hierarchysync forces provisioning
474                throw new StatusException("SyncCollections->CheckForChanges(): PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY);
475
476            // Check if a hierarchy sync is necessary
477            if (ZPush::GetDeviceManager()->IsHierarchySyncRequired())
478                throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::ERROR_WRONG_HIERARCHY);
479
480            // Check if there are newer requests
481            // If so, this process should be terminated if more than 60 secs to go
482            if ($pingTracking->DoForcePingTimeout()) {
483                // do not update CPOs because another process has already read them!
484                $this->saveData = false;
485
486                // more than 60 secs to go?
487                if (($now + 60) < $endat) {
488                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss due to other process", ($now-$started), $lifetime));
489                    ZPush::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds", ($now-$started)), true);
490                    return false;
491                }
492            }
493
494            // Use changes sink if available
495            if ($changesSink) {
496                // in some occasions we do realize a full export to see if there are pending changes
497                // every 5 minutes this is also done to see if there were "missed" notifications
498                if (SINK_FORCERECHECK !== false && $forceRealExport+SINK_FORCERECHECK <= $now) {
499                    if ($this->CountChanges($onlyPingable)) {
500                        ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found relevant changes on regular export");
501                        return true;
502                    }
503                    $forceRealExport = $now;
504                }
505
506                ZPush::GetTopCollector()->AnnounceInformation(sprintf("Sink %d/%ds on %s", ($now-$started), $lifetime, $checkClasses));
507                $notifications = ZPush::GetBackend()->ChangesSink($nextInterval);
508
509                $validNotifications = false;
510                foreach ($notifications as $folderid) {
511                    // check if the notification on the folder is within our filter
512                    if ($this->CountChange($folderid)) {
513                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s'", $folderid));
514                        $validNotifications = true;
515                        $this->waitingTime = time()-$started;
516                    }
517                    else {
518                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s', but it is not relevant", $folderid));
519                    }
520                }
521                if ($validNotifications)
522                    return true;
523            }
524            // use polling mechanism
525            else {
526                ZPush::GetTopCollector()->AnnounceInformation(sprintf("Polling %d/%ds on %s", ($now-$started), $lifetime, $checkClasses));
527                if ($this->CountChanges($onlyPingable)) {
528                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Found changes polling"));
529                    return true;
530                }
531                else {
532                    sleep($nextInterval);
533                }
534            } // end polling
535        } // end wait for changes
536        ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): no changes found after %ds", time() - $started));
537
538        return false;
539    }
540
541    /**
542     * Checks if the currently known collections for
543     * changes performing Exporter->GetChangeCount()
544     *
545     * @param boolean   $onlyPingable   (opt) only check for folders which have the PingableFlag
546     *
547     * @access public
548     * @return boolean      indicating if changes were found or not
549     */
550    public function CountChanges($onlyPingable = false) {
551        $changesAvailable = false;
552        foreach ($this->collections as $folderid => $spa) {
553            if ($onlyPingable && $spa->GetPingableFlag() !== true)
554                continue;
555
556            if (isset($this->addparms[$spa->GetFolderId()]["status"]) && $this->addparms[$spa->GetFolderId()]["status"] != SYNC_STATUS_SUCCESS)
557                continue;
558
559            if ($this->CountChange($folderid))
560                $changesAvailable = true;
561        }
562
563        return $changesAvailable;
564    }
565
566    /**
567     * Checks a folder for changes performing Exporter->GetChangeCount()
568     *
569     * @param string    $folderid   counts changes for a folder
570     *
571     * @access private
572     * @return boolean      indicating if changes were found or not
573     */
574     private function CountChange($folderid) {
575        $spa = $this->GetCollection($folderid);
576
577        // switch user store if this is a additional folder (additional true -> do not debug)
578        ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid, true));
579        $changecount = false;
580
581        try {
582            $exporter = ZPush::GetBackend()->GetExporter($folderid);
583            if ($exporter !== false && isset($this->addparms[$folderid]["state"])) {
584                $importer = false;
585
586                $exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA);
587                $exporter->ConfigContentParameters($spa->GetCPO());
588                $ret = $exporter->InitializeExporter($importer);
589
590                if ($ret !== false)
591                    $changecount = $exporter->GetChangeCount();
592            }
593        }
594        catch (StatusException $ste) {
595            throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
596        }
597
598        // start over if exporter can not be configured atm
599        if ($changecount === false )
600            ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter.");
601
602        $this->changes[$folderid] = $changecount;
603
604        if(isset($this->addparms[$folderid]['savestate'])) {
605            try {
606                // Discard any data
607                while(is_array($exporter->Synchronize()));
608                $this->addparms[$folderid]['savestate'] = $exporter->GetState();
609            }
610            catch (StatusException $ste) {
611                throw new StatusException("SyncCollections->CountChange(): could not get new state from exporter", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
612            }
613        }
614
615        return ($changecount > 0);
616     }
617
618    /**
619     * Returns an array with all folderid and the amount of changes found
620     *
621     * @access public
622     * @return array
623     */
624    public function GetChangedFolderIds() {
625        return $this->changes;
626    }
627
628    /**
629     * Indicates if the process did wait in a sink, polling or before running a
630     * regular export to find changes
631     *
632     * @access public
633     * @return array
634     */
635    public function WaitedForChanges() {
636        ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->WaitedForChanges: waited for %d seconds", $this->waitingTime));
637        return ($this->waitingTime > 0);
638    }
639
640    /**
641     * Simple Iterator Interface implementation to traverse through collections
642     */
643
644    /**
645     * Rewind the Iterator to the first element
646     *
647     * @access public
648     * @return
649     */
650    public function rewind() {
651        return reset($this->collections);
652    }
653
654    /**
655     * Returns the current element
656     *
657     * @access public
658     * @return mixed
659     */
660    public function current() {
661        return current($this->collections);
662    }
663
664    /**
665     * Return the key of the current element
666     *
667     * @access public
668     * @return scalar on success, or NULL on failure.
669     */
670    public function key() {
671        return key($this->collections);
672    }
673
674    /**
675     * Move forward to next element
676     *
677     * @access public
678     * @return
679     */
680    public function next() {
681        return next($this->collections);
682    }
683
684    /**
685     * Checks if current position is valid
686     *
687     * @access public
688     * @return boolean
689     */
690    public function valid() {
691        return (key($this->collections) !== null);
692    }
693
694    /**
695     * Gets the StateManager from the DeviceManager
696     * if it's not available
697     *
698     * @access private
699     * @return
700     */
701     private function loadStateManager() {
702         if (!isset($this->stateManager))
703            $this->stateManager = ZPush::GetDeviceManager()->GetStateManager();
704     }
705}
706
707?>
Note: See TracBrowser for help on using the repository browser.