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 - 2016 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. 22* 23* This program is distributed in the hope that it will be useful, 24* but WITHOUT ANY WARRANTY; without even the implied warranty of 25* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26* GNU Affero General Public License for more details. 27* 28* You should have received a copy of the GNU Affero General Public License 29* along with this program. If not, see <http://www.gnu.org/licenses/>. 30* 31* Consult LICENSE file for details 32************************************************/ 33 34class SyncCollections implements Iterator { 35 const ERROR_NO_COLLECTIONS = 1; 36 const ERROR_WRONG_HIERARCHY = 2; 37 const OBSOLETE_CONNECTION = 3; 38 const HIERARCHY_CHANGED = 4; 39 40 private $stateManager; 41 42 private $collections = array(); 43 private $addparms = array(); 44 private $changes = array(); 45 private $saveData = true; 46 47 private $refPolicyKey = false; 48 private $refLifetime = false; 49 50 private $globalWindowSize; 51 private $lastSyncTime; 52 53 private $waitingTime = 0; 54 private $hierarchyExporterChecked = false; 55 private $loggedGlobalWindowSizeOverwrite = false; 56 57 58 /** 59 * Invalidates all pingable flags for all folders. 60 * 61 * @access public 62 * @return boolean 63 */ 64 static public function InvalidatePingableFlags() { 65 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections::InvalidatePingableFlags(): Invalidating now"); 66 try { 67 $sc = new SyncCollections(); 68 $sc->LoadAllCollections(); 69 foreach ($sc as $folderid => $spa) { 70 if ($spa->GetPingableFlag() == true) { 71 $spa->DelPingableFlag(); 72 $sc->SaveCollection($spa); 73 } 74 } 75 return true; 76 } 77 catch (ZPushException $e) {} 78 return false; 79 } 80 81 /** 82 * Constructor 83 */ 84 public function __construct() { 85 } 86 87 /** 88 * Sets the StateManager for this object 89 * If this is not done and a method needs it, the StateManager will be 90 * requested from the DeviceManager 91 * 92 * @param StateManager $statemanager 93 * 94 * @access public 95 * @return 96 */ 97 public function SetStateManager($statemanager) { 98 $this->stateManager = $statemanager; 99 } 100 101 /** 102 * Loads all collections known for the current device 103 * 104 * @param boolean $overwriteLoaded (opt) overwrites Collection with saved state if set to true 105 * @param boolean $loadState (opt) indicates if the collection sync state should be loaded, default false 106 * @param boolean $checkPermissions (opt) if set to true each folder will pass 107 * through a backend->Setup() to check permissions. 108 * If this fails a StatusException will be thrown. 109 * @param boolean $loadHierarchy (opt) if the hierarchy sync states should be loaded, default false 110 * @param boolean $confirmedOnly (opt) indicates if only confirmed states should be loaded, default: false 111 * 112 * @access public 113 * @throws StatusException with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails 114 * @throws StateInvalidException if the sync state can not be found or relation between states is invalid ($loadState = true) 115 * @return boolean 116 */ 117 public function LoadAllCollections($overwriteLoaded = false, $loadState = false, $checkPermissions = false, $loadHierarchy = false, $confirmedOnly = false) { 118 $this->loadStateManager(); 119 120 // this operation should not remove old state counters 121 $this->stateManager->DoNotDeleteOldStates(); 122 123 $invalidStates = false; 124 foreach($this->stateManager->GetSynchedFolders() as $folderid) { 125 if ($overwriteLoaded === false && isset($this->collections[$folderid])) 126 continue; 127 128 // Load Collection! 129 if (! $this->LoadCollection($folderid, $loadState, $checkPermissions, $confirmedOnly)) 130 $invalidStates = true; 131 } 132 133 // load the hierarchy data - there are no permissions to verify so we just set it to false 134 if ($loadHierarchy && !$this->LoadCollection(false, $loadState, false, false)) 135 throw new StatusException("Invalid states found while loading hierarchy data. Forcing hierarchy sync"); 136 137 if ($invalidStates) 138 throw new StateInvalidException("Invalid states found while loading collections. Forcing sync"); 139 140 return true; 141 } 142 143 /** 144 * Loads all collections known for the current device 145 * 146 * @param string $folderid folder id to be loaded 147 * @param boolean $loadState (opt) indicates if the collection sync state should be loaded, default true 148 * @param boolean $checkPermissions (opt) if set to true each folder will pass 149 * through a backend->Setup() to check permissions. 150 * If this fails a StatusException will be thrown. 151 * @param boolean $confirmedOnly (opt) indicates if only confirmed states should be loaded, default: false 152 * 153 * @access public 154 * @throws StatusException with SyncCollections::ERROR_WRONG_HIERARCHY if permission check fails 155 * @throws StateInvalidException if the sync state can not be found or relation between states is invalid ($loadState = true) 156 * @return boolean 157 */ 158 public function LoadCollection($folderid, $loadState = false, $checkPermissions = false, $confirmedOnly = false) { 159 $this->loadStateManager(); 160 161 try { 162 // Get SyncParameters for the folder from the state 163 $spa = $this->stateManager->GetSynchedFolderState($folderid, !$loadState); 164 165 // TODO remove resync of folders for < Z-Push 2 beta4 users 166 // this forces a resync of all states previous to Z-Push 2 beta4 167 if (! $spa instanceof SyncParameters) { 168 throw new StateInvalidException("Saved state are not of type SyncParameters"); 169 } 170 171 if ($spa->GetUuidCounter() == 0) { 172 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->LoadCollection(): Found collection with move state only, ignoring."); 173 return true; 174 } 175 } 176 catch (StateInvalidException $sive) { 177 // in case there is something wrong with the state, just stop here 178 // later when trying to retrieve the SyncParameters nothing will be found 179 180 if ($folderid === false) { 181 throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not get FOLDERDATA state of the hierarchy uuid: %s", $spa->GetUuid()), self::ERROR_WRONG_HIERARCHY); 182 } 183 184 // we also generate a fake change, so a sync on this folder is triggered 185 $this->changes[$folderid] = 1; 186 187 return false; 188 } 189 190 // if this is an additional folder the backend has to be setup correctly 191 if ($checkPermissions === true && ! ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()))) 192 throw new StatusException(sprintf("SyncCollections->LoadCollection(): could not Setup() the backend for folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), self::ERROR_WRONG_HIERARCHY); 193 194 // add collection to object 195 $addStatus = $this->AddCollection($spa); 196 197 // load the latest known syncstate if requested 198 if ($addStatus && $loadState === true) { 199 try { 200 // make sure the hierarchy cache is loaded when we are loading hierarchy states 201 $this->addparms[$folderid]["state"] = $this->stateManager->GetSyncState($spa->GetLatestSyncKey($confirmedOnly), ($folderid === false)); 202 } 203 catch (StateNotFoundException $snfe) { 204 // if we can't find the state, first we should try a sync of that folder, so 205 // we generate a fake change, so a sync on this folder is triggered 206 $this->changes[$folderid] = 1; 207 208 // make sure this folder is fully synched on next Sync request 209 $this->invalidateFolderStat($spa); 210 211 return false; 212 } 213 } 214 215 return $addStatus; 216 } 217 218 /** 219 * Saves a SyncParameters Object 220 * 221 * @param SyncParamerts $spa 222 * 223 * @access public 224 * @return boolean 225 */ 226 public function SaveCollection($spa) { 227 if (! $this->saveData || !$spa->HasFolderId()) 228 return false; 229 230 if ($spa->IsDataChanged()) { 231 $this->loadStateManager(); 232 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->SaveCollection(): Data of folder '%s' changed", $spa->GetFolderId())); 233 234 // save new windowsize 235 if (isset($this->globalWindowSize)) 236 $spa->SetWindowSize($this->globalWindowSize); 237 238 // update latest lifetime 239 if (isset($this->refLifetime)) 240 $spa->SetReferenceLifetime($this->refLifetime); 241 242 return $this->stateManager->SetSynchedFolderState($spa); 243 } 244 return false; 245 } 246 247 /** 248 * Adds a SyncParameters object to the current list of collections 249 * 250 * @param SyncParameters $spa 251 * 252 * @access public 253 * @return boolean 254 */ 255 public function AddCollection($spa) { 256 if (! $spa->HasFolderId()) 257 return false; 258 259 if ($spa->GetKoeGabFolder() === true) { 260 // put KOE GAB at the beginning of the sync 261 $this->collections = [$spa->GetFolderId() => $spa] + $this->collections; 262 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->AddCollection(): Prioritizing KOE GAB folder for synchronization"); 263 } 264 else { 265 $this->collections[$spa->GetFolderId()] = $spa; 266 } 267 268 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Folder id '%s' : ref. PolicyKey '%s', ref. Lifetime '%s', last sync at '%s'", $spa->GetFolderId(), $spa->GetReferencePolicyKey(), $spa->GetReferenceLifetime(), $spa->GetLastSyncTime())); 269 if ($spa->HasLastSyncTime() && $spa->GetLastSyncTime() > $this->lastSyncTime) { 270 $this->lastSyncTime = $spa->GetLastSyncTime(); 271 272 // use SyncParameters PolicyKey as reference if available 273 if ($spa->HasReferencePolicyKey()) 274 $this->refPolicyKey = $spa->GetReferencePolicyKey(); 275 276 // use SyncParameters LifeTime as reference if available 277 if ($spa->HasReferenceLifetime()) 278 $this->refLifetime = $spa->GetReferenceLifetime(); 279 280 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->AddCollection(): Updated reference PolicyKey '%s', reference Lifetime '%s', Last sync at '%s'", $this->refPolicyKey, $this->refLifetime, $this->lastSyncTime)); 281 } 282 283 return true; 284 } 285 286 /** 287 * Returns a previousily added or loaded SyncParameters object for a folderid 288 * 289 * @param SyncParameters $spa 290 * 291 * @access public 292 * @return SyncParameters / boolean false if no SyncParameters object is found for folderid 293 */ 294 public function GetCollection($folderid) { 295 if (isset($this->collections[$folderid])) 296 return $this->collections[$folderid]; 297 else 298 return false; 299 } 300 301 /** 302 * Indicates if there are any loaded CPOs 303 * 304 * @access public 305 * @return boolean 306 */ 307 public function HasCollections() { 308 return ! empty($this->collections); 309 } 310 311 /** 312 * Indicates the amount of collections loaded. 313 * 314 * @access public 315 * @return int 316 */ 317 public function GetCollectionCount() { 318 return count($this->collections); 319 } 320 321 /** 322 * Add a non-permanent key/value pair for a SyncParameters object 323 * 324 * @param SyncParameters $spa target SyncParameters 325 * @param string $key 326 * @param mixed $value 327 * 328 * @access public 329 * @return boolean 330 */ 331 public function AddParameter($spa, $key, $value) { 332 if (!$spa->HasFolderId()) 333 return false; 334 335 $folderid = $spa->GetFolderId(); 336 if (!isset($this->addparms[$folderid])) 337 $this->addparms[$folderid] = array(); 338 339 $this->addparms[$folderid][$key] = $value; 340 return true; 341 } 342 343 /** 344 * Returns a previousily set non-permanent value for a SyncParameters object 345 * 346 * @param SyncParameters $spa target SyncParameters 347 * @param string $key 348 * 349 * @access public 350 * @return mixed returns 'null' if nothing set 351 */ 352 public function GetParameter($spa, $key) { 353 if (!$spa->HasFolderId()) 354 return null; 355 356 if (isset($this->addparms[$spa->GetFolderId()]) && isset($this->addparms[$spa->GetFolderId()][$key])) 357 return $this->addparms[$spa->GetFolderId()][$key]; 358 else 359 return null; 360 } 361 362 /** 363 * Returns the latest known PolicyKey to be used as reference 364 * 365 * @access public 366 * @return int/boolen returns false if nothing found in collections 367 */ 368 public function GetReferencePolicyKey() { 369 return $this->refPolicyKey; 370 } 371 372 /** 373 * Sets a global window size which should be used for all collections 374 * in a case of a heartbeat and/or partial sync 375 * 376 * @param int $windowsize 377 * 378 * @access public 379 * @return boolean 380 */ 381 public function SetGlobalWindowSize($windowsize) { 382 $this->globalWindowSize = $windowsize; 383 return true; 384 } 385 386 /** 387 * Returns the global window size of items to be exported in total over all 388 * requested collections. 389 * 390 * @access public 391 * @return int/boolean returns requested windows size, 512 (max) or the 392 * value of config SYNC_MAX_ITEMS if it is lower 393 */ 394 public function GetGlobalWindowSize() { 395 // take the requested global windowsize or the max 512 if not defined 396 if (isset($this->globalWindowSize)) { 397 $globalWindowSize = $this->globalWindowSize; 398 } 399 else { 400 $globalWindowSize = WINDOW_SIZE_MAX; // 512 by default 401 } 402 403 if (defined("SYNC_MAX_ITEMS") && SYNC_MAX_ITEMS < $globalWindowSize) { 404 if (!$this->loggedGlobalWindowSizeOverwrite) { 405 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->GetGlobalWindowSize() overwriting requested global window size of %d by %d forced in configuration.", $globalWindowSize, SYNC_MAX_ITEMS)); 406 $this->loggedGlobalWindowSizeOverwrite = true; 407 } 408 $globalWindowSize = SYNC_MAX_ITEMS; 409 } 410 411 return $globalWindowSize; 412 } 413 414 /** 415 * Sets the lifetime for heartbeat or ping connections 416 * 417 * @param int $lifetime time in seconds 418 * 419 * @access public 420 * @return boolean 421 */ 422 public function SetLifetime($lifetime) { 423 $this->refLifetime = $lifetime; 424 return true; 425 } 426 427 /** 428 * Sets the lifetime for heartbeat or ping connections 429 * previousily set or saved in a collection 430 * 431 * @access public 432 * @return int returns PING_HIGHER_BOUND_LIFETIME as default if nothing set or not available. 433 * If PING_HIGHER_BOUND_LIFETIME is not set, returns 600. 434 */ 435 public function GetLifetime() { 436 if (!isset($this->refLifetime) || $this->refLifetime === false) { 437 if (PING_HIGHER_BOUND_LIFETIME !== false) { 438 return PING_HIGHER_BOUND_LIFETIME; 439 } 440 return 600; 441 } 442 443 return $this->refLifetime; 444 } 445 446 /** 447 * Returns the timestamp of the last synchronization for all 448 * loaded collections 449 * 450 * @access public 451 * @return int timestamp 452 */ 453 public function GetLastSyncTime() { 454 return $this->lastSyncTime; 455 } 456 457 /** 458 * Checks if the currently known collections for changes for $lifetime seconds. 459 * If the backend provides a ChangesSink the sink will be used. 460 * If not every $interval seconds an exporter will be configured for each 461 * folder to perform GetChangeCount(). 462 * 463 * @param int $lifetime (opt) total lifetime to wait for changes / default 600s 464 * @param int $interval (opt) time between blocking operations of sink or polling / default 30s 465 * @param boolean $onlyPingable (opt) only check for folders which have the PingableFlag 466 * 467 * @access public 468 * @return boolean indicating if changes were found 469 * @throws StatusException with code SyncCollections::ERROR_NO_COLLECTIONS if no collections available 470 * with code SyncCollections::ERROR_WRONG_HIERARCHY if there were errors getting changes 471 */ 472 public function CheckForChanges($lifetime = 600, $interval = 30, $onlyPingable = false) { 473 $classes = array(); 474 foreach ($this->collections as $folderid => $spa){ 475 if ($onlyPingable && $spa->GetPingableFlag() !== true || ! $folderid) 476 continue; 477 478 // the class name will be overwritten for KOE-GAB 479 $class = $this->getPingClass($spa); 480 481 if (!isset($classes[$class])) 482 $classes[$class] = 0; 483 $classes[$class] += 1; 484 } 485 if (empty($classes)) 486 $checkClasses = "policies only"; 487 else if (array_sum($classes) > 4) { 488 $checkClasses = ""; 489 foreach($classes as $class=>$count) { 490 if ($count == 1) 491 $checkClasses .= sprintf("%s ", $class); 492 else 493 $checkClasses .= sprintf("%s(%d) ", $class, $count); 494 } 495 } 496 else 497 $checkClasses = implode(" ", array_keys($classes)); 498 499 $pingTracking = new PingTracking(); 500 $this->changes = array(); 501 502 ZPush::GetDeviceManager()->AnnounceProcessAsPush(); 503 ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true); 504 ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes))?'policy':'store', $lifetime)); 505 506 // use changes sink where available 507 $changesSink = ZPush::GetBackend()->HasChangesSink(); 508 509 // create changessink and check folder stats if there are folders to Ping 510 if (!empty($classes)) { 511 // initialize all possible folders 512 foreach ($this->collections as $folderid => $spa) { 513 if (($onlyPingable && $spa->GetPingableFlag() !== true) || ! $folderid) 514 continue; 515 516 $backendFolderId = $spa->GetBackendFolderId(); 517 518 // get the user store if this is a additional folder 519 $store = ZPush::GetAdditionalSyncFolderStore($backendFolderId); 520 521 // initialize sink if no immediate changes were found so far 522 if ($changesSink && empty($this->changes)) { 523 ZPush::GetBackend()->Setup($store); 524 if (! ZPush::GetBackend()->ChangesSinkInitialize($backendFolderId)) 525 throw new StatusException(sprintf("Error initializing ChangesSink for folder id %s/%s", $folderid, $backendFolderId), self::ERROR_WRONG_HIERARCHY); 526 } 527 528 // check if the folder stat changed since the last sync, if so generate a change for it (only on first run) 529 $currentFolderStat = ZPush::GetBackend()->GetFolderStat($store, $backendFolderId); 530 if ($this->waitingTime == 0 && ZPush::GetBackend()->HasFolderStats() && $currentFolderStat !== false && $spa->IsExporterRunRequired($currentFolderStat, true)) { 531 $this->changes[$spa->GetFolderId()] = 1; 532 } 533 } 534 } 535 536 if (!empty($this->changes)) { 537 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found changes verifying the folder stats"); 538 return true; 539 } 540 541 // wait for changes 542 $started = time(); 543 $endat = time() + $lifetime; 544 545 // always use policy key from the request if it was sent 546 $policyKey = $this->GetReferencePolicyKey(); 547 if (Request::WasPolicyKeySent() && Request::GetPolicyKey() != 0) { 548 ZLog::Write(LOGLEVEL_DEBUG, sprintf("refpolkey:'%s', sent polkey:'%s'", $policyKey, Request::GetPolicyKey())); 549 $policyKey = Request::GetPolicyKey(); 550 } 551 while(($now = time()) < $endat) { 552 // how long are we waiting for changes 553 $this->waitingTime = $now-$started; 554 555 $nextInterval = $interval; 556 // we should not block longer than the lifetime 557 if ($endat - $now < $nextInterval) 558 $nextInterval = $endat - $now; 559 560 // Check if provisioning is necessary 561 // if a PolicyKey was sent use it. If not, compare with the ReferencePolicyKey 562 if (PROVISIONING === true && $policyKey !== false && ZPush::GetDeviceManager()->ProvisioningRequired($policyKey, true, false)) 563 // the hierarchysync forces provisioning 564 throw new StatusException("SyncCollections->CheckForChanges(): Policies or PolicyKey changed. Provisioning required.", self::ERROR_WRONG_HIERARCHY); 565 566 // Check if a hierarchy sync is necessary 567 if ($this->countHierarchyChange()) 568 throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::HIERARCHY_CHANGED); 569 570 // Check if there are newer requests 571 // If so, this process should be terminated if more than 60 secs to go 572 if ($pingTracking->DoForcePingTimeout()) { 573 // do not update CPOs because another process has already read them! 574 $this->saveData = false; 575 576 // more than 60 secs to go? 577 if (($now + 60) < $endat) { 578 ZPush::GetTopCollector()->AnnounceInformation(sprintf("Forced timeout after %ds", ($now-$started)), true); 579 throw new StatusException(sprintf("SyncCollections->CheckForChanges(): Timeout forced after %ss from %ss due to other process", ($now-$started), $lifetime), self::OBSOLETE_CONNECTION); 580 } 581 } 582 583 // Use changes sink if available 584 if ($changesSink) { 585 ZPush::GetTopCollector()->AnnounceInformation(sprintf("Sink %d/%ds on %s", ($now-$started), $lifetime, $checkClasses)); 586 $notifications = ZPush::GetBackend()->ChangesSink($nextInterval); 587 588 // how long are we waiting for changes 589 $this->waitingTime = time()-$started; 590 591 $validNotifications = false; 592 foreach ($notifications as $backendFolderId) { 593 // Check hierarchy notifications 594 if ($backendFolderId === IBackend::HIERARCHYNOTIFICATION) { 595 // wait two seconds before validating this notification, because it could potentially be made by the mobile and we need some time to update the states. 596 sleep(2); 597 // check received hierarchy notifications by exporting 598 if ($this->countHierarchyChange(true)) { 599 throw new StatusException("SyncCollections->CheckForChanges(): HierarchySync required.", self::HIERARCHY_CHANGED); 600 } 601 } 602 else { 603 // the backend will notify on the backend folderid 604 $folderid = ZPush::GetDeviceManager()->GetFolderIdForBackendId($backendFolderId); 605 606 // check if the notification on the folder is within our filter 607 if ($this->CountChange($folderid)) { 608 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s'", $folderid)); 609 $validNotifications = true; 610 $this->waitingTime = time()-$started; 611 } 612 else { 613 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Notification received on folder '%s', but it is not relevant", $folderid)); 614 } 615 } 616 } 617 if ($validNotifications) 618 return true; 619 } 620 // use polling mechanism 621 else { 622 ZPush::GetTopCollector()->AnnounceInformation(sprintf("Polling %d/%ds on %s", ($now-$started), $lifetime, $checkClasses)); 623 if ($this->CountChanges($onlyPingable)) { 624 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): Found changes polling")); 625 return true; 626 } 627 else { 628 sleep($nextInterval); 629 } 630 } // end polling 631 } // end wait for changes 632 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CheckForChanges(): no changes found after %ds", time() - $started)); 633 634 return false; 635 } 636 637 /** 638 * Checks if the currently known collections for 639 * changes performing Exporter->GetChangeCount() 640 * 641 * @param boolean $onlyPingable (opt) only check for folders which have the PingableFlag 642 * 643 * @access public 644 * @return boolean indicating if changes were found or not 645 */ 646 public function CountChanges($onlyPingable = false) { 647 $changesAvailable = false; 648 foreach ($this->collections as $folderid => $spa) { 649 if ($onlyPingable && $spa->GetPingableFlag() !== true) 650 continue; 651 652 if (isset($this->addparms[$spa->GetFolderId()]["status"]) && $this->addparms[$spa->GetFolderId()]["status"] != SYNC_STATUS_SUCCESS) 653 continue; 654 655 if ($this->CountChange($folderid)) 656 $changesAvailable = true; 657 } 658 659 return $changesAvailable; 660 } 661 662 /** 663 * Checks a folder for changes performing Exporter->GetChangeCount() 664 * 665 * @param string $folderid counts changes for a folder 666 * 667 * @access private 668 * @return boolean indicating if changes were found or not 669 */ 670 private function CountChange($folderid) { 671 $spa = $this->GetCollection($folderid); 672 673 if (!$spa) { 674 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CountChange(): Could not get SyncParameters object from cache for folderid '%s' to verify notification. Ignoring.", $folderid)); 675 return false; 676 } 677 678 // Prevent ZP-623 by checking if the states have been used before, if so force a sync on this folder. 679 // ZCP/KC 7.2.3 and newer support SYNC_STATE_READONLY so this behaviour is not required (see ZP-968). 680 if (!Utils::CheckMapiExtVersion('7.2.3')) { 681 if (ZPush::GetDeviceManager()->CheckHearbeatStateIntegrity($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter())) { 682 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CountChange(): Cannot verify changes for state as it was already used. Forcing sync of folder."); 683 $this->changes[$folderid] = 1; 684 return true; 685 } 686 } 687 688 $backendFolderId = ZPush::GetDeviceManager()->GetBackendIdForFolderId($folderid); 689 // switch user store if this is a additional folder (additional true -> do not debug) 690 ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($backendFolderId, true)); 691 $changecount = false; 692 693 try { 694 $exporter = ZPush::GetBackend()->GetExporter($backendFolderId); 695 if ($exporter !== false && isset($this->addparms[$folderid]["state"])) { 696 $importer = false; 697 698 $exporter->SetMoveStates($spa->GetMoveState()); 699 $exporter->Config($this->addparms[$folderid]["state"], BACKEND_DISCARD_DATA); 700 $exporter->ConfigContentParameters($spa->GetCPO()); 701 $ret = $exporter->InitializeExporter($importer); 702 703 if ($ret !== false) 704 $changecount = $exporter->GetChangeCount(); 705 } 706 } 707 catch (StatusException $ste) { 708 if ($ste->getCode() == SYNC_STATUS_FOLDERHIERARCHYCHANGED) { 709 ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): exporter can not be re-configured due to state error, emulating change in folder to force Sync."); 710 $this->changes[$folderid] = 1; 711 // make sure this folder is fully synched on next Sync request 712 $this->invalidateFolderStat($spa); 713 714 return true; 715 } 716 throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); 717 } 718 719 // start over if exporter can not be configured atm 720 if ($changecount === false) 721 ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter."); 722 723 $this->changes[$folderid] = $changecount; 724 725 return ($changecount > 0); 726 } 727 728 /** 729 * Checks the hierarchy for changes. 730 * 731 * @param boolean export changes, default: false 732 * 733 * @access private 734 * @return boolean indicating if changes were found or not 735 */ 736 private function countHierarchyChange($exportChanges = false) { 737 $folderid = false; 738 739 // Check with device manager if the hierarchy should be reloaded. 740 // New additional folders are loaded here. 741 if (ZPush::GetDeviceManager()->IsHierarchySyncRequired()) { 742 ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->countHierarchyChange(): DeviceManager says HierarchySync is required."); 743 return true; 744 } 745 746 $changecount = false; 747 if ($exportChanges || $this->hierarchyExporterChecked === false) { 748 try { 749 // if this is a validation (not first run), make sure to load the hierarchy data again 750 if ($this->hierarchyExporterChecked === true && !$this->LoadCollection(false, true, false)) 751 throw new StatusException("Invalid states found while re-loading hierarchy data."); 752 753 754 $changesMem = ZPush::GetDeviceManager()->GetHierarchyChangesWrapper(); 755 // the hierarchyCache should now fully be initialized - check for changes in the additional folders 756 $changesMem->Config(ZPush::GetAdditionalSyncFolders(false)); 757 758 // reset backend to the main store 759 ZPush::GetBackend()->Setup(false); 760 $exporter = ZPush::GetBackend()->GetExporter(); 761 if ($exporter !== false && isset($this->addparms[$folderid]["state"])) { 762 $exporter->Config($this->addparms[$folderid]["state"]); 763 $ret = $exporter->InitializeExporter($changesMem); 764 while(is_array($exporter->Synchronize())); 765 766 if ($ret !== false) 767 $changecount = $changesMem->GetChangeCount(); 768 769 $this->hierarchyExporterChecked = true; 770 } 771 } 772 catch (StatusException $ste) { 773 throw new StatusException("SyncCollections->countHierarchyChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); 774 } 775 776 // start over if exporter can not be configured atm 777 if ($changecount === false ) 778 ZLog::Write(LOGLEVEL_WARN, "SyncCollections->countHierarchyChange(): no changes received from Exporter."); 779 } 780 return ($changecount > 0); 781 } 782 783 /** 784 * Returns an array with all folderid and the amount of changes found 785 * 786 * @access public 787 * @return array 788 */ 789 public function GetChangedFolderIds() { 790 return $this->changes; 791 } 792 793 /** 794 * Indicates if there are folders which are pingable 795 * 796 * @access public 797 * @return boolean 798 */ 799 public function PingableFolders() { 800 foreach ($this->collections as $folderid => $spa) { 801 if ($spa->GetPingableFlag() == true) { 802 return true; 803 } 804 } 805 806 return false; 807 } 808 809 /** 810 * Indicates if the process did wait in a sink, polling or before running a 811 * regular export to find changes 812 * 813 * @access public 814 * @return boolean 815 */ 816 public function WaitedForChanges() { 817 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->WaitedForChanges: waited for %d seconds", $this->waitingTime)); 818 return ($this->waitingTime > 0); 819 } 820 821 /** 822 * Indicates how many seconds the process did wait in a sink, polling or before running a 823 * regular export to find changes. 824 * 825 * @access public 826 * @return int 827 */ 828 public function GetWaitedSeconds() { 829 return $this->waitingTime; 830 } 831 832 /** 833 * Returns how the current folder should be called in the PING comment. 834 * 835 * @param SyncParameters $spa 836 * 837 * @access public 838 * @return string 839 */ 840 private function getPingClass($spa) { 841 $class = $spa->GetContentClass(); 842 if ($class == "Calendar" && strpos($spa->GetFolderId(), DeviceManager::FLD_ORIGIN_GAB) === 0) { 843 $class = "GAB"; 844 } 845 return $class; 846 } 847 848 /** 849 * Simple Iterator Interface implementation to traverse through collections 850 */ 851 852 /** 853 * Rewind the Iterator to the first element 854 * 855 * @access public 856 * @return 857 */ 858 public function rewind() { 859 return reset($this->collections); 860 } 861 862 /** 863 * Returns the current element 864 * 865 * @access public 866 * @return mixed 867 */ 868 public function current() { 869 return current($this->collections); 870 } 871 872 /** 873 * Return the key of the current element 874 * 875 * @access public 876 * @return scalar on success, or NULL on failure. 877 */ 878 public function key() { 879 return key($this->collections); 880 } 881 882 /** 883 * Move forward to next element 884 * 885 * @access public 886 * @return 887 */ 888 public function next() { 889 return next($this->collections); 890 } 891 892 /** 893 * Checks if current position is valid 894 * 895 * @access public 896 * @return boolean 897 */ 898 public function valid() { 899 return (key($this->collections) != null && key($this->collections) != false); 900 } 901 902 /** 903 * Gets the StateManager from the DeviceManager 904 * if it's not available 905 * 906 * @access private 907 * @return 908 */ 909 private function loadStateManager() { 910 if (!isset($this->stateManager)) 911 $this->stateManager = ZPush::GetDeviceManager()->GetStateManager(); 912 } 913 914 /** 915 * Remove folder statistics from a SyncParameter object. 916 * 917 * @param SyncParameters $spa 918 * 919 * @access public 920 * @return 921 */ 922 private function invalidateFolderStat($spa) { 923 if($spa->HasFolderStat()) { 924 ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->invalidateFolderStat(): removing folder stat '%s' for folderid '%s'", $spa->GetFolderStat(), $spa->GetFolderId())); 925 $spa->DelFolderStat(); 926 $this->SaveCollection($spa); 927 return true; 928 } 929 return false; 930 } 931} 932