Back to index

d-push  2.0
synccollections.php
Go to the documentation of this file.
00001 <?php
00002 /***********************************************
00003 * File      :   synccollections.php
00004 * Project   :   Z-Push
00005 * Descr     :   This is basically a list of synched folders with it's
00006 *               respective SyncParameters, while some additional parameters
00007 *               which are not stored there can be kept here.
00008 *               The class also provides CheckForChanges which is basically
00009 *               a loop through all collections checking for changes.
00010 *               SyncCollections is used for Sync (with and without heartbeat)
00011 *               and Ping connections.
00012 *               To check for changes in Heartbeat and Ping requeste the same
00013 *               sync states as for the default synchronization are used.
00014 *
00015 * Created   :   06.01.2012
00016 *
00017 * Copyright 2007 - 2012 Zarafa Deutschland GmbH
00018 *
00019 * This program is free software: you can redistribute it and/or modify
00020 * it under the terms of the GNU Affero General Public License, version 3,
00021 * as published by the Free Software Foundation with the following additional
00022 * term according to sec. 7:
00023 *
00024 * According to sec. 7 of the GNU Affero General Public License, version 3,
00025 * the terms of the AGPL are supplemented with the following terms:
00026 *
00027 * "Zarafa" is a registered trademark of Zarafa B.V.
00028 * "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
00029 * The licensing of the Program under the AGPL does not imply a trademark license.
00030 * Therefore any rights, title and interest in our trademarks remain entirely with us.
00031 *
00032 * However, if you propagate an unmodified version of the Program you are
00033 * allowed to use the term "Z-Push" to indicate that you distribute the Program.
00034 * Furthermore you may use our trademarks where it is necessary to indicate
00035 * the intended purpose of a product or service provided you use it in accordance
00036 * with honest practices in industrial or commercial matters.
00037 * If you want to propagate modified versions of the Program under the name "Z-Push",
00038 * you may only do so if you have a written permission by Zarafa Deutschland GmbH
00039 * (to acquire a permission please contact Zarafa at trademark@zarafa.com).
00040 *
00041 * This program is distributed in the hope that it will be useful,
00042 * but WITHOUT ANY WARRANTY; without even the implied warranty of
00043 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00044 * GNU Affero General Public License for more details.
00045 *
00046 * You should have received a copy of the GNU Affero General Public License
00047 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00048 *
00049 * Consult LICENSE file for details
00050 ************************************************/
00051 
00052 
00053 class SyncCollections implements Iterator {
00054     const ERROR_NO_COLLECTIONS = 1;
00055     const ERROR_WRONG_HIERARCHY = 2;
00056 
00057     private $stateManager;
00058 
00059     private $collections = array();
00060     private $addparms = array();
00061     private $changes = array();
00062     private $saveData = true;
00063 
00064     private $refPolicyKey = false;
00065     private $refLifetime = false;
00066 
00067     private $globalWindowSize;
00068     private $lastSyncTime;
00069 
00070     private $waitingTime = 0;
00071 
00072 
00076     public function SyncCollections() {
00077     }
00078 
00089     public function SetStateManager($statemanager) {
00090         $this->stateManager = $statemanager;
00091     }
00092 
00107     public function LoadAllCollections($overwriteLoaded = false, $loadState = false, $checkPermissions = false) {
00108         $this->loadStateManager();
00109 
00110         $invalidStates = false;
00111         foreach($this->stateManager->GetSynchedFolders() as $folderid) {
00112             if ($overwriteLoaded === false && isset($this->collections[$folderid]))
00113                 continue;
00114 
00115             // Load Collection!
00116             if (! $this->LoadCollection($folderid, $loadState, $checkPermissions))
00117                 $invalidStates = true;
00118         }
00119 
00120         if ($invalidStates)
00121             throw new StateInvalidException("Invalid states found while loading collections. Forcing sync");
00122 
00123         return true;
00124     }
00125 
00140     public function LoadCollection($folderid, $loadState = false, $checkPermissions = false) {
00141         $this->loadStateManager();
00142 
00143         try {
00144             // Get SyncParameters for the folder from the state
00145             $spa = $this->stateManager->GetSynchedFolderState($folderid);
00146 
00147             // TODO remove resync of folders for < Z-Push 2 beta4 users
00148             // this forces a resync of all states previous to Z-Push 2 beta4
00149             if (! $spa instanceof SyncParameters)
00150                 throw new StateInvalidException("Saved state are not of type SyncParameters");
00151         }
00152         catch (StateInvalidException $sive) {
00153             // in case there is something wrong with the state, just stop here
00154             // later when trying to retrieve the SyncParameters nothing will be found
00155 
00156             // we also generate a fake change, so a sync on this folder is triggered
00157             $this->changes[$folderid] = 1;
00158 
00159             return false;
00160         }
00161 
00162         // if this is an additional folder the backend has to be setup correctly
00163         if ($checkPermissions === true && ! ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId())))
00164             throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), self::ERROR_WRONG_HIERARCHY);
00165 
00166         // add collection to object
00167         $this->AddCollection($spa);
00168 
00169         // load the latest known syncstate if requested
00170         if ($loadState === true)
00171             $this->addparms[$folderid]["state"] = $this->stateManager->GetSyncState($spa->GetLatestSyncKey());
00172 
00173         return true;
00174     }
00175 
00184     public function SaveCollection($spa) {
00185         if (! $this->saveData)
00186             return false;
00187 
00188         if ($spa->IsDataChanged()) {
00189             $this->loadStateManager();
00190             ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->SaveCollection(): Data of folder '%s' changed", $spa->GetFolderId()));
00191 
00192             // save new windowsize
00193             if (isset($this->globalWindowSize))
00194                 $spa->SetWindowSize($this->globalWindowSize);
00195 
00196             // update latest lifetime
00197             if (isset($this->refLifetime))
00198                 $spa->SetReferenceLifetime($this->refLifetime);
00199 
00200             return $this->stateManager->SetSynchedFolderState($spa);
00201         }
00202         return false;
00203     }
00204 
00213     public function AddCollection($spa) {
00214         $this->collections[$spa->GetFolderId()] = $spa;
00215 
00216         if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) {
00217             $this->lastSyncTime = $spa->GetLastSyncTime();
00218 
00219             // use SyncParameters PolicyKey as reference if available
00220             if ($spa->HasReferencePolicyKey())
00221                 $this->refPolicyKey = $spa->GetReferencePolicyKey();
00222 
00223             // use SyncParameters LifeTime as reference if available
00224             if ($spa->HasReferenceLifetime())
00225                 $this->refLifetime = $spa->GetReferenceLifetime();
00226         }
00227 
00228         return true;
00229     }
00230 
00239     public function GetCollection($folderid) {
00240         if (isset($this->collections[$folderid]))
00241             return $this->collections[$folderid];
00242         else
00243             return false;
00244     }
00245 
00252     public function HasCollections() {
00253         return ! empty($this->collections);
00254     }
00255 
00266     public function AddParameter($spa, $key, $value) {
00267         if (!$spa->HasFolderId())
00268             return false;
00269 
00270         $folderid = $spa->GetFolderId();
00271         if (!isset($this->addparms[$folderid]))
00272             $this->addparms[$folderid] = array();
00273 
00274         $this->addparms[$folderid][$key] = $value;
00275         return true;
00276     }
00277 
00287     public function GetParameter($spa, $key) {
00288         if (isset($this->addparms[$spa->GetFolderId()]) && isset($this->addparms[$spa->GetFolderId()][$key]))
00289             return $this->addparms[$spa->GetFolderId()][$key];
00290         else
00291             return null;
00292     }
00293 
00300     public function GetReferencePolicyKey() {
00301         return $this->refPolicyKey;
00302     }
00303 
00313     public function SetGlobalWindowSize($windowsize) {
00314         $this->globalWindowSize = $windowsize;
00315         return true;
00316     }
00317 
00325     public function GetGlobalWindowSize() {
00326         if (!isset($this->globalWindowSize))
00327             return false;
00328 
00329         return $this->globalWindowSize;
00330     }
00331 
00340     public function SetLifetime($lifetime) {
00341         $this->refLifetime = $lifetime;
00342         return true;
00343     }
00344 
00352     public function GetLifetime() {
00353         if (!isset( $this->refLifetime) || $this->refLifetime === false)
00354             return 600;
00355 
00356         return $this->refLifetime;
00357     }
00358 
00366     public function GetLastSyncTime() {
00367         return $this->lastSyncTime;
00368     }
00369 
00378     static public function GetLastSyncTimeOfDevice(&$device) {
00379         // we need a StateManager for this operation
00380         $stateManager = new StateManager();
00381         $stateManager->SetDevice($device);
00382 
00383         $sc = new SyncCollections();
00384         $sc->SetStateManager($stateManager);
00385 
00386         // load all collections of device without loading states or checking permissions
00387         $sc->LoadAllCollections(true, false, false);
00388 
00389         return $sc->GetLastSyncTime();
00390     }
00391 
00407     public function CheckForChanges($lifetime = 600, $interval = 30, $onlyPingable = false) {
00408         $classes = array();
00409         foreach ($this->collections as $folderid => $spa){
00410             if ($onlyPingable && $spa->GetPingableFlag() !== true)
00411                 continue;
00412 
00413             $classes[] = $spa->GetContentClass();
00414         }
00415         $checkClasses = implode("/", $classes);
00416 
00417         // is there something to check?
00418         if (empty($this->collections) || ($onlyPingable && empty($classes)))
00419             throw new StatusException("SyncCollections->CheckForChanges(): no collections available", self::ERROR_NO_COLLECTIONS);
00420 
00421         $pingTracking = new PingTracking();
00422         $this->changes = array();
00423         $changesAvailable = false;
00424 
00425         ZPush::GetTopCollector()->SetAsPushConnection();
00426         ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true);
00427         ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for changes... (lifetime %d seconds)", $lifetime));
00428 
00429         // use changes sink where available
00430         $changesSink = false;
00431         $forceRealExport = 0;
00432         if (ZPush::GetBackend()->HasChangesSink()) {
00433             $changesSink = true;
00434 
00435             // initialize all possible folders
00436             foreach ($this->collections as $folderid => $spa) {
00437                 if ($onlyPingable && $spa->GetPingableFlag() !== true)
00438                     continue;
00439 
00440                 // switch user store if this is a additional folder and initialize sink
00441                 ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid));
00442                 if (! ZPush::GetBackend()->ChangesSinkInitialize($folderid))
00443                     throw new StatusException(sprintf("Error initializing ChangesSink for folder id '%s'", $folderid), self::ERROR_WRONG_HIERARCHY);
00444             }
00445         }
00446 
00447         // wait for changes
00448         $started = time();
00449         $endat = time() + $lifetime;
00450         while(($now = time()) < $endat) {
00451             // how long are we waiting for changes
00452             $this->waitingTime = $now-$started;
00453 
00454             $nextInterval = $interval;
00455             // we should not block longer than the lifetime
00456             if ($endat - $now < $nextInterval)
00457                 $nextInterval = $endat - $now;
00458 
00459             // Check if provisioning is necessary
00460             // if a PolicyKey was sent use it. If not, compare with the ReferencePolicyKey
00461             if (PROVISIONING === true && $this->GetReferencePolicyKey() !== false && ZPush::GetDeviceManager()->ProvisioningRequired($this->GetReferencePolicyKey(), true))
00462                 // the hierarchysync forces provisioning
00463                 throw new StatusException("SyncCollections->CheckForChanges(): PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY);
00464 
00465             // Check if a hierarchy sync is necessary
00466             if (ZPush::GetDeviceManager()->IsHierarchySyncRequired())
00467                 throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::ERROR_WRONG_HIERARCHY);
00468 
00469             // Check if there are newer requests
00470             // If so, this process should be terminated if more than 60 secs to go
00471             if ($pingTracking->DoForcePingTimeout()) {
00472                 // do not update CPOs because another process has already read them!
00473                 $this->saveData = false;
00474 
00475                 // more than 60 secs to go?
00476                 if (($now + 60) < $endat) {
00477                     ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss due to other process", ($now-$started), $lifetime));
00478                     ZPush::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds", ($now-$started)), true);
00479                     return false;
00480                 }
00481             }
00482 
00483             // Use changes sink if available
00484             if ($changesSink) {
00485                 // in some occasions we do realize a full export to see if there are pending changes
00486                 // every 5 minutes this is also done to see if there were "missed" notifications
00487                 if (SINK_FORCERECHECK !== false && $forceRealExport+SINK_FORCERECHECK <= $now) {
00488                     if ($this->CountChanges($onlyPingable)) {
00489                         ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found relevant changes on regular export");
00490                         return true;
00491                     }
00492                     $forceRealExport = $now;
00493                 }
00494 
00495                 ZPush::GetTopCollector()->AnnounceInformation(sprintf("Sink %d/%ds on %s", ($now-$started), $lifetime, $checkClasses));
00496                 $notifications = ZPush::GetBackend()->ChangesSink($nextInterval);
00497 
00498                 $validNotifications = false;
00499                 foreach ($notifications as $folderid) {
00500                     // check if the notification on the folder is within our filter
00501                     if ($this->CountChange($folderid)) {
00502                         ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s'", $folderid));
00503                         $validNotifications = true;
00504                     }
00505                     else {
00506                         ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s', but it is not relevant", $folderid));
00507                     }
00508                 }
00509                 if ($validNotifications)
00510                     return true;
00511             }
00512             // use polling mechanism
00513             else {
00514                 ZPush::GetTopCollector()->AnnounceInformation(sprintf("Polling %d/%ds on %s", ($now-$started), $lifetime, $checkClasses));
00515                 if ($this->CountChanges($onlyPingable)) {
00516                     ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Found changes polling"));
00517                     return true;
00518                 }
00519                 else {
00520                     sleep($nextInterval);
00521                 }
00522             } // end polling
00523         } // end wait for changes
00524         ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): no changes found after %ds", time() - $started));
00525 
00526         return false;
00527     }
00528 
00538     public function CountChanges($onlyPingable = false) {
00539         $changesAvailable = false;
00540         foreach ($this->collections as $folderid => $spa) {
00541             if ($onlyPingable && $spa->GetPingableFlag() !== true)
00542                 continue;
00543 
00544             if (isset($this->addparms[$spa->GetFolderId()]["status"]) && $this->addparms[$spa->GetFolderId()]["status"] != SYNC_STATUS_SUCCESS)
00545                 continue;
00546 
00547             if ($this->CountChange($folderid))
00548                 $changesAvailable = true;
00549         }
00550 
00551         return $changesAvailable;
00552     }
00553 
00562      private function CountChange($folderid) {
00563         $spa = $this->GetCollection($folderid);
00564 
00565         // switch user store if this is a additional folder (additional true -> do not debug)
00566         ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid, true));
00567         $changecount = false;
00568 
00569         try {
00570             $exporter = ZPush::GetBackend()->GetExporter($folderid);
00571             if ($exporter !== false && isset($this->addparms[$folderid]["state"])) {
00572                 $importer = false;
00573 
00574                 $exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA);
00575                 $exporter->ConfigContentParameters($spa->GetCPO());
00576                 $ret = $exporter->InitializeExporter($importer);
00577 
00578                 if ($ret !== false)
00579                     $changecount = $exporter->GetChangeCount();
00580             }
00581         }
00582         catch (StatusException $ste) {
00583             throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
00584         }
00585 
00586         // start over if exporter can not be configured atm
00587         if ($changecount === false )
00588             ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter.");
00589 
00590         $this->changes[$folderid] = $changecount;
00591 
00592         if(isset($this->addparms[$folderid]['savestate'])) {
00593             try {
00594                 // Discard any data
00595                 while(is_array($exporter->Synchronize()));
00596                 $this->addparms[$folderid]['savestate'] = $exporter->GetState();
00597             }
00598             catch (StatusException $ste) {
00599                 throw new StatusException("SyncCollections->CountChange(): could not get new state from exporter", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
00600             }
00601         }
00602 
00603         return ($changecount > 0);
00604      }
00605 
00612     public function GetChangedFolderIds() {
00613         return $this->changes;
00614     }
00615 
00623     public function WaitedForChanges() {
00624         return ($this->waitingTime > 1);
00625     }
00626 
00637     public function rewind() {
00638         return reset($this->collections);
00639     }
00640 
00647     public function current() {
00648         return current($this->collections);
00649     }
00650 
00657     public function key() {
00658         return key($this->collections);
00659     }
00660 
00667     public function next() {
00668         return next($this->collections);
00669     }
00670 
00677     public function valid() {
00678         return (key($this->collections) !== null);
00679     }
00680 
00688      private function loadStateManager() {
00689          if (!isset($this->stateManager))
00690             $this->stateManager = ZPush::GetDeviceManager()->GetStateManager();
00691      }
00692 }
00693 
00694 ?>