/** * Invalidates all pingable flags for all folders. * * @access public * @return boolean */ public static function InvalidatePingableFlags() { ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections::InvalidatePingableFlags(): Invalidating now"); try { $sc = new SyncCollections(); $sc->LoadAllCollections(); foreach ($sc as $folderid => $spa) { if ($spa->GetPingableFlag() == true) { $spa->DelPingableFlag(); $sc->SaveCollection($spa); } } return true; } catch (ZPushException $e) { } return false; }
/** * Handles the Ping command * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { $interval = defined('PING_INTERVAL') && PING_INTERVAL > 0 ? PING_INTERVAL : 30; $pingstatus = false; $fakechanges = array(); $foundchanges = false; // Contains all requested folders (containers) $sc = new SyncCollections(); // Load all collections - do load states and check permissions try { $sc->LoadAllCollections(true, true, true); } catch (StateNotFoundException $snfex) { $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; self::$topCollector->AnnounceInformation("StateNotFoundException: require HierarchySync", true); } catch (StateInvalidException $snfex) { // we do not have a ping status for this, but SyncCollections should have generated fake changes for the folders which are broken $fakechanges = $sc->GetChangedFolderIds(); $foundchanges = true; self::$topCollector->AnnounceInformation("StateInvalidException: force sync", true); } catch (StatusException $stex) { $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; self::$topCollector->AnnounceInformation("StatusException: require HierarchySync", true); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandlePing(): reference PolicyKey for PING: %s", $sc->GetReferencePolicyKey())); // receive PING initialization data if (self::$decoder->getElementStartTag(SYNC_PING_PING)) { self::$topCollector->AnnounceInformation("Processing PING data"); ZLog::Write(LOGLEVEL_DEBUG, "HandlePing(): initialization data received"); if (self::$decoder->getElementStartTag(SYNC_PING_LIFETIME)) { $sc->SetLifetime(self::$decoder->getElementContent()); self::$decoder->getElementEndTag(); } if (($el = self::$decoder->getElementStartTag(SYNC_PING_FOLDERS)) && $el[EN_FLAGS] & EN_FLAGS_CONTENT) { // remove PingableFlag from all collections foreach ($sc as $folderid => $spa) { $spa->DelPingableFlag(); } while (self::$decoder->getElementStartTag(SYNC_PING_FOLDER)) { while (1) { if (self::$decoder->getElementStartTag(SYNC_PING_SERVERENTRYID)) { $folderid = self::$decoder->getElementContent(); self::$decoder->getElementEndTag(); } if (self::$decoder->getElementStartTag(SYNC_PING_FOLDERTYPE)) { $class = self::$decoder->getElementContent(); self::$decoder->getElementEndTag(); } $e = self::$decoder->peek(); if ($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); break; } } $spa = $sc->GetCollection($folderid); if (!$spa) { // The requested collection is not synchronized. // check if the HierarchyCache is available, if not, trigger a HierarchySync try { self::$deviceManager->GetFolderClassFromCacheByID($folderid); } catch (NoHierarchyCacheAvailableException $nhca) { ZLog::Write(LOGLEVEL_INFO, sprintf("HandlePing(): unknown collection '%s', triggering HierarchySync", $folderid)); $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; } // Trigger a Sync request because then the device will be forced to resync this folder. $fakechanges[$folderid] = 1; $foundchanges = true; } else { if ($class == $spa->GetContentClass()) { $spa->SetPingableFlag(true); ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandlePing(): using saved sync state for '%s' id '%s'", $spa->GetContentClass(), $folderid)); } } } if (!self::$decoder->getElementEndTag()) { return false; } } if (!self::$decoder->getElementEndTag()) { return false; } // save changed data foreach ($sc as $folderid => $spa) { $sc->SaveCollection($spa); } } else { // if no ping initialization data was sent, we check if we have pingable folders // if not, we indicate that there is nothing to do. if (!$sc->PingableFolders()) { $pingstatus = SYNC_PINGSTATUS_FAILINGPARAMS; ZLog::Write(LOGLEVEL_DEBUG, "HandlePing(): no pingable folders found and no initialization data sent. Returning SYNC_PINGSTATUS_FAILINGPARAMS."); } } // Check for changes on the default LifeTime, set interval and ONLY on pingable collections try { if (!$pingstatus && empty($fakechanges)) { $foundchanges = $sc->CheckForChanges($sc->GetLifetime(), $interval, true); } } catch (StatusException $ste) { switch ($ste->getCode()) { case SyncCollections::ERROR_NO_COLLECTIONS: $pingstatus = SYNC_PINGSTATUS_FAILINGPARAMS; break; case SyncCollections::ERROR_WRONG_HIERARCHY: $pingstatus = SYNC_PINGSTATUS_FOLDERHIERSYNCREQUIRED; self::$deviceManager->AnnounceProcessStatus(false, $pingstatus); break; case SyncCollections::OBSOLETE_CONNECTION: $foundchanges = false; break; } } self::$encoder->StartWBXML(); self::$encoder->startTag(SYNC_PING_PING); self::$encoder->startTag(SYNC_PING_STATUS); if (isset($pingstatus) && $pingstatus) { self::$encoder->content($pingstatus); } else { self::$encoder->content($foundchanges ? SYNC_PINGSTATUS_CHANGES : SYNC_PINGSTATUS_HBEXPIRED); } self::$encoder->endTag(); if (!$pingstatus) { self::$encoder->startTag(SYNC_PING_FOLDERS); if (empty($fakechanges)) { $changes = $sc->GetChangedFolderIds(); } else { $changes = $fakechanges; } foreach ($changes as $folderid => $changecount) { if ($changecount > 0) { self::$encoder->startTag(SYNC_PING_FOLDER); self::$encoder->content($folderid); self::$encoder->endTag(); if (empty($fakechanges)) { self::$topCollector->AnnounceInformation(sprintf("Found change in %s", $sc->GetCollection($folderid)->GetContentClass()), true); } } } self::$encoder->endTag(); } self::$encoder->endTag(); return true; }
/** * Handles the Sync command * Performs the synchronization of messages * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { // Contains all requested folders (containers) $sc = new SyncCollections(); $status = SYNC_STATUS_SUCCESS; $wbxmlproblem = false; $emptysync = false; // check if the hierarchySync was fully completed if (USE_PARTIAL_FOLDERSYNC) { if (self::$deviceManager->GetFolderSyncComplete() === false) { ZLog::Write(LOGLEVEL_INFO, "Request->HandleSync(): Sync request aborted, as exporting of folders has not yet completed"); self::$topCollector->AnnounceInformation("Aborted due incomplete folder sync", true); $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; } else { ZLog::Write(LOGLEVEL_INFO, "Request->HandleSync(): FolderSync marked as complete"); } } // Start Synchronize if (self::$decoder->getElementStartTag(SYNC_SYNCHRONIZE)) { // AS 1.0 sends version information in WBXML if (self::$decoder->getElementStartTag(SYNC_VERSION)) { $sync_version = self::$decoder->getElementContent(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("WBXML sync version: '%s'", $sync_version)); if (!self::$decoder->getElementEndTag()) { return false; } } // Synching specified folders // Android still sends heartbeat sync even if all syncfolders are disabled. // Check if Folders tag is empty (<Folders/>) and only sync if there are // some folders in the request. See ZP-172 $startTag = self::$decoder->getElementStartTag(SYNC_FOLDERS); if (isset($startTag[EN_FLAGS]) && $startTag[EN_FLAGS]) { while (self::$decoder->getElementStartTag(SYNC_FOLDER)) { $actiondata = array(); $actiondata["requested"] = true; $actiondata["clientids"] = array(); $actiondata["modifyids"] = array(); $actiondata["removeids"] = array(); $actiondata["fetchids"] = array(); $actiondata["statusids"] = array(); // read class, synckey and folderid without SyncParameters Object for now $class = $synckey = $folderid = false; //for AS versions < 2.5 if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { $class = self::$decoder->getElementContent(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sync folder: '%s'", $class)); if (!self::$decoder->getElementEndTag()) { return false; } } // SyncKey if (self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { $synckey = "0"; if (($synckey = self::$decoder->getElementContent()) !== false) { if (!self::$decoder->getElementEndTag()) { return false; } } } else { return false; } // FolderId if (self::$decoder->getElementStartTag(SYNC_FOLDERID)) { $folderid = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } } // compatibility mode AS 1.0 - get folderid which was sent during GetHierarchy() if (!$folderid && $class) { $folderid = self::$deviceManager->GetFolderIdFromCacheByClass($class); } // folderid HAS TO BE known by now, so we retrieve the correct SyncParameters object for an update try { $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState($folderid); // TODO remove resync of folders for < Z-Push 2 beta4 users // this forces a resync of all states previous to Z-Push 2 beta4 if (!$spa instanceof SyncParameters) { throw new StateInvalidException("Saved state are not of type SyncParameters"); } // new/resync requested if ($synckey == "0") { $spa->RemoveSyncKey(); } else { if ($synckey !== false) { $spa->SetSyncKey($synckey); } } } catch (StateInvalidException $stie) { $spa = new SyncParameters(); $status = SYNC_STATUS_INVALIDSYNCKEY; self::$topCollector->AnnounceInformation("State invalid - Resync folder", true); self::$deviceManager->ForceFolderResync($folderid); } // update folderid.. this might be a new object $spa->SetFolderId($folderid); if ($class !== false) { $spa->SetContentClass($class); } // Get class for as versions >= 12.0 if (!$spa->HasContentClass()) { try { $spa->SetContentClass(self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId())); ZLog::Write(LOGLEVEL_DEBUG, sprintf("GetFolderClassFromCacheByID from Device Manager: '%s' for id:'%s'", $spa->GetContentClass(), $spa->GetFolderId())); } catch (NoHierarchyCacheAvailableException $nhca) { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; self::$deviceManager->ForceFullResync(); } } // done basic SPA initialization/loading -> add to SyncCollection $sc->AddCollection($spa); $sc->AddParameter($spa, "requested", true); if ($spa->HasContentClass()) { self::$topCollector->AnnounceInformation(sprintf("%s request", $spa->GetContentClass()), true); } else { ZLog::Write(LOGLEVEL_WARN, "Not possible to determine class of request. Request did not contain class and apparently there is an issue with the HierarchyCache."); } // SUPPORTED properties if (($se = self::$decoder->getElementStartTag(SYNC_SUPPORTED)) !== false) { // ZP-481: LG phones send an empty supported tag, so only read the contents if available here // if <Supported/> is received, it's as no supported fields would have been sent at all. // unsure if this is the correct approach, or if in this case some default list should be used if ($se[EN_FLAGS] & EN_FLAGS_CONTENT) { $supfields = array(); while (1) { $el = self::$decoder->getElement(); if ($el[EN_TYPE] == EN_TYPE_ENDTAG) { break; } else { $supfields[] = $el[EN_TAG]; } } self::$deviceManager->SetSupportedFields($spa->GetFolderId(), $supfields); } } // Deletes as moves can be an empty tag as well as have value if (self::$decoder->getElementStartTag(SYNC_DELETESASMOVES)) { $spa->SetDeletesAsMoves(true); if (($dam = self::$decoder->getElementContent()) !== false) { $spa->SetDeletesAsMoves((bool) $dam); if (!self::$decoder->getElementEndTag()) { return false; } } } // Get changes can be an empty tag as well as have value // code block partly contributed by dw2412 if (self::$decoder->getElementStartTag(SYNC_GETCHANGES)) { $sc->AddParameter($spa, "getchanges", true); if (($gc = self::$decoder->getElementContent()) !== false) { $sc->AddParameter($spa, "getchanges", $gc); if (!self::$decoder->getElementEndTag()) { return false; } } } if (self::$decoder->getElementStartTag(SYNC_WINDOWSIZE)) { $ws = self::$decoder->getElementContent(); // normalize windowsize - see ZP-477 if ($ws == 0 || $ws > 512) { $ws = 512; } $spa->SetWindowSize($ws); // also announce the currently requested window size to the DeviceManager self::$deviceManager->SetWindowSize($spa->GetFolderId(), $spa->GetWindowSize()); if (!self::$decoder->getElementEndTag()) { return false; } } // conversation mode requested if (self::$decoder->getElementStartTag(SYNC_CONVERSATIONMODE)) { $spa->SetConversationMode(true); if (($conversationmode = self::$decoder->getElementContent()) !== false) { $spa->SetConversationMode((bool) $conversationmode); if (!self::$decoder->getElementEndTag()) { return false; } } } // Do not truncate by default $spa->SetTruncation(SYNC_TRUNCATION_ALL); // use default conflict handling if not specified by the mobile $spa->SetConflict(SYNC_CONFLICT_DEFAULT); while (self::$decoder->getElementStartTag(SYNC_OPTIONS)) { $firstOption = true; while (1) { // foldertype definition if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { $foldertype = self::$decoder->getElementContent(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): specified options block with foldertype '%s'", $foldertype)); // switch the foldertype for the next options $spa->UseCPO($foldertype); // set to synchronize all changes. The mobile could overwrite this value $spa->SetFilterType(SYNC_FILTERTYPE_ALL); if (!self::$decoder->getElementEndTag()) { return false; } } else { if ($firstOption) { $spa->UseCPO(); // set to synchronize all changes. The mobile could overwrite this value $spa->SetFilterType(SYNC_FILTERTYPE_ALL); } } $firstOption = false; if (self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { $spa->SetFilterType(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_TRUNCATION)) { $spa->SetTruncation(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_RTFTRUNCATION)) { $spa->SetRTFTruncation(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_MIMESUPPORT)) { $spa->SetMimeSupport(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_MIMETRUNCATION)) { $spa->SetMimeTruncation(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_CONFLICT)) { $spa->SetConflict(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } while (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODYPREFERENCE)) { if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { $bptype = self::$decoder->getElementContent(); $spa->BodyPreference($bptype); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TRUNCATIONSIZE)) { $spa->BodyPreference($bptype)->SetTruncationSize(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_ALLORNONE)) { $spa->BodyPreference($bptype)->SetAllOrNone(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_PREVIEW)) { $spa->BodyPreference($bptype)->SetPreview(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (!self::$decoder->getElementEndTag()) { return false; } } $e = self::$decoder->peek(); if ($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); break; } } } // limit items to be synchronized to the mobiles if configured if (defined('SYNC_FILTERTIME_MAX') && SYNC_FILTERTIME_MAX > SYNC_FILTERTYPE_ALL && (!$spa->HasFilterType() || $spa->GetFilterType() == SYNC_FILTERTYPE_ALL || $spa->GetFilterType() > SYNC_FILTERTIME_MAX)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("SYNC_FILTERTIME_MAX defined. Filter set to value: %s", SYNC_FILTERTIME_MAX)); $spa->SetFilterType(SYNC_FILTERTIME_MAX); } // Check if the hierarchycache is available. If not, trigger a HierarchySync if (self::$deviceManager->IsHierarchySyncRequired()) { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; ZLog::Write(LOGLEVEL_DEBUG, "HierarchyCache is also not available. Triggering HierarchySync to device"); } if (($el = self::$decoder->getElementStartTag(SYNC_PERFORM)) && $el[EN_FLAGS] & EN_FLAGS_CONTENT) { // We can not proceed here as the content class is unknown if ($status != SYNC_STATUS_SUCCESS) { ZLog::Write(LOGLEVEL_WARN, "Ignoring all incoming actions as global status indicates problem."); $wbxmlproblem = true; break; } $performaction = true; // unset the importer $this->importer = false; $nchanges = 0; while (1) { // ADD, MODIFY, REMOVE or FETCH $element = self::$decoder->getElement(); if ($element[EN_TYPE] != EN_TYPE_STARTTAG) { self::$decoder->ungetElement($element); break; } if ($status == SYNC_STATUS_SUCCESS) { $nchanges++; } // Foldertype sent when synching SMS if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { $foldertype = self::$decoder->getElementContent(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): incoming data with foldertype '%s'", $foldertype)); if (!self::$decoder->getElementEndTag()) { return false; } } else { $foldertype = false; } $serverid = false; if (self::$decoder->getElementStartTag(SYNC_SERVERENTRYID)) { if (($serverid = self::$decoder->getElementContent()) !== false) { if (!self::$decoder->getElementEndTag()) { // end serverid return false; } } } if (self::$decoder->getElementStartTag(SYNC_CLIENTENTRYID)) { $clientid = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { // end clientid return false; } } else { $clientid = false; } // Get the SyncMessage if sent if (self::$decoder->getElementStartTag(SYNC_DATA)) { $message = ZPush::getSyncObjectFromFolderClass($spa->GetContentClass()); $message->Decode(self::$decoder); // set Ghosted fields $message->emptySupported(self::$deviceManager->GetSupportedFields($spa->GetFolderId())); if (!self::$decoder->getElementEndTag()) { // end applicationdata return false; } } else { $message = false; } switch ($element[EN_TAG]) { case SYNC_FETCH: array_push($actiondata["fetchids"], $serverid); break; default: // get the importer if ($this->importer == false) { $status = $this->getImporter($sc, $spa, $actiondata); } if ($status == SYNC_STATUS_SUCCESS) { $this->importMessage($spa, $actiondata, $element[EN_TAG], $message, $clientid, $serverid, $foldertype, $nchanges); } else { ZLog::Write(LOGLEVEL_WARN, "Ignored incoming change, global status indicates problem."); } break; } if ($actiondata["fetchids"]) { self::$topCollector->AnnounceInformation(sprintf("Fetching %d", $nchanges)); } else { self::$topCollector->AnnounceInformation(sprintf("Incoming %d", $nchanges)); } if (!self::$decoder->getElementEndTag()) { // end add/change/delete/move return false; } } if ($status == SYNC_STATUS_SUCCESS && $this->importer !== false) { ZLog::Write(LOGLEVEL_INFO, sprintf("Sync->Handle(): Processed %d incoming changes", $nchanges)); if (!$actiondata["fetchids"]) { self::$topCollector->AnnounceInformation(sprintf("%d incoming", $nchanges), true); } try { // Save the updated state, which is used for the exporter later $sc->AddParameter($spa, "state", $this->importer->GetState()); } catch (StatusException $stex) { $status = $stex->getCode(); } } if (!self::$decoder->getElementEndTag()) { // end PERFORM return false; } } // save the failsave state if (!empty($actiondata["statusids"])) { unset($actiondata["failstate"]); $actiondata["failedsyncstate"] = $sc->GetParameter($spa, "state"); self::$deviceManager->GetStateManager()->SetSyncFailState($actiondata); } // save actiondata $sc->AddParameter($spa, "actiondata", $actiondata); if (!self::$decoder->getElementEndTag()) { // end collection return false; } // AS14 does not send GetChanges anymore. We should do it if there were no incoming changes if (!isset($performaction) && !$sc->GetParameter($spa, "getchanges") && $spa->HasSyncKey()) { $sc->AddParameter($spa, "getchanges", true); } } // END FOLDER if (!$wbxmlproblem && !self::$decoder->getElementEndTag()) { // end collections return false; } } // end FOLDERS if (self::$decoder->getElementStartTag(SYNC_HEARTBEATINTERVAL)) { $hbinterval = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { // SYNC_HEARTBEATINTERVAL return false; } } if (self::$decoder->getElementStartTag(SYNC_WAIT)) { $wait = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { // SYNC_WAIT return false; } // internally the heartbeat interval and the wait time are the same // heartbeat is in seconds, wait in minutes $hbinterval = $wait * 60; } if (self::$decoder->getElementStartTag(SYNC_WINDOWSIZE)) { $sc->SetGlobalWindowSize(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { // SYNC_WINDOWSIZE return false; } } if (self::$decoder->getElementStartTag(SYNC_PARTIAL)) { $partial = true; } else { $partial = false; } if (!$wbxmlproblem && !self::$decoder->getElementEndTag()) { // end sync return false; } } else { $emptysync = true; } // END SYNCHRONIZE // check heartbeat/wait time if (isset($hbinterval)) { if ($hbinterval < 60 || $hbinterval > 3540) { $status = SYNC_STATUS_INVALIDWAITORHBVALUE; ZLog::Write(LOGLEVEL_WARN, sprintf("HandleSync(): Invalid heartbeat or wait value '%s'", $hbinterval)); } } // Partial & Empty Syncs need saved data to proceed with synchronization if ($status == SYNC_STATUS_SUCCESS && ($emptysync === true || $partial === true)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Partial or Empty sync requested. Retrieving data of synchronized folders.")); // Load all collections - do not overwrite existing (received!), load states and check permissions try { $sc->LoadAllCollections(false, true, true); } catch (StateNotFoundException $snfex) { $status = SYNC_STATUS_INVALIDSYNCKEY; self::$topCollector->AnnounceInformation("StateNotFoundException", true); } catch (StatusException $stex) { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); } // update a few values foreach ($sc as $folderid => $spa) { // manually set getchanges parameter for this collection $sc->AddParameter($spa, "getchanges", true); // set new global windowsize without marking the SPA as changed if ($sc->GetGlobalWindowSize()) { $spa->SetWindowSize($sc->GetGlobalWindowSize(), false); } // announce WindowSize to DeviceManager self::$deviceManager->SetWindowSize($folderid, $spa->GetWindowSize()); } if (!$sc->HasCollections()) { $status = SYNC_STATUS_SYNCREQUESTINCOMPLETE; } } // HEARTBEAT & Empty sync if ($status == SYNC_STATUS_SUCCESS && (isset($hbinterval) || $emptysync == true)) { $interval = defined('PING_INTERVAL') && PING_INTERVAL > 0 ? PING_INTERVAL : 30; if (isset($hbinterval)) { $sc->SetLifetime($hbinterval); } // states are lazy loaded - we have to make sure that they are there! $loadstatus = SYNC_STATUS_SUCCESS; foreach ($sc as $folderid => $spa) { // some androids do heartbeat on the OUTBOX folder, with weird results - ZP-362 // we do not load the state so we will never get relevant changes on the OUTBOX folder if (self::$deviceManager->GetFolderTypeFromCacheById($folderid) == SYNC_FOLDER_TYPE_OUTBOX) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Heartbeat on Outbox folder not allowed")); continue; } $fad = array(); // if loading the states fails, we do not enter heartbeat, but we keep $status on SYNC_STATUS_SUCCESS // so when the changes are exported the correct folder gets an SYNC_STATUS_INVALIDSYNCKEY if ($loadstatus == SYNC_STATUS_SUCCESS) { $loadstatus = $this->loadStates($sc, $spa, $fad); } } if ($loadstatus == SYNC_STATUS_SUCCESS) { $foundchanges = false; try { // always check for changes ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Entering Heartbeat mode")); $foundchanges = $sc->CheckForChanges($sc->GetLifetime(), $interval); } catch (StatusException $stex) { if ($stex->getCode() == SyncCollections::OBSOLETE_CONNECTION) { $status = SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID; } else { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); } } // in case there are no changes and no other request has synchronized while we waited, we can reply with an empty response if (!$foundchanges && $status == SYNC_STATUS_SUCCESS) { // if there were changes to the SPA or CPOs we need to save this before we terminate // only save if the state was not modified by some other request, if so, return state invalid status foreach ($sc as $folderid => $spa) { if (self::$deviceManager->CheckHearbeatStateIntegrity($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter())) { $status = SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID; } else { $sc->SaveCollection($spa); } } if ($status == SYNC_STATUS_SUCCESS) { ZLog::Write(LOGLEVEL_DEBUG, "No changes found and no other process changed states. Replying with empty response and closing connection."); self::$specialHeaders = array(); self::$specialHeaders[] = "Connection: close"; return true; } } if ($foundchanges) { foreach ($sc->GetChangedFolderIds() as $folderid => $changecount) { // check if there were other sync requests for a folder during the heartbeat $spa = $sc->GetCollection($folderid); if ($changecount > 0 && $sc->WaitedForChanges() && self::$deviceManager->CheckHearbeatStateIntegrity($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter())) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): heartbeat: found %d changes in '%s' which was already synchronized. Heartbeat aborted!", $changecount, $folderid)); $status = SYNC_COMMONSTATUS_SYNCSTATEVERSIONINVALID; } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): heartbeat: found %d changes in '%s'", $changecount, $folderid)); } } } } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Start Output")); // Start the output self::$encoder->startWBXML(); self::$encoder->startTag(SYNC_SYNCHRONIZE); // global status // SYNC_COMMONSTATUS_* start with values from 101 if ($status != SYNC_COMMONSTATUS_SUCCESS && $status > 100) { self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($status); self::$encoder->endTag(); } else { self::$encoder->startTag(SYNC_FOLDERS); foreach ($sc as $folderid => $spa) { // get actiondata $actiondata = $sc->GetParameter($spa, "actiondata"); if ($status == SYNC_STATUS_SUCCESS && (!$spa->GetContentClass() || !$spa->GetFolderId())) { ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): no content class or folderid found for collection.")); continue; } if (!$sc->GetParameter($spa, "requested")) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): partial sync for folder class '%s' with id '%s'", $spa->GetContentClass(), $spa->GetFolderId())); } // initialize exporter to get changecount $changecount = false; if (isset($exporter)) { unset($exporter); } // TODO we could check against $sc->GetChangedFolderIds() on heartbeat so we do not need to configure all exporter again if ($status == SYNC_STATUS_SUCCESS && ($sc->GetParameter($spa, "getchanges") || !$spa->HasSyncKey())) { //make sure the states are loaded $status = $this->loadStates($sc, $spa, $actiondata); if ($status == SYNC_STATUS_SUCCESS) { try { // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) { throw new StatusException(sprintf("HandleSync() could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); } // Use the state from the importer, as changes may have already happened $exporter = self::$backend->GetExporter($spa->GetFolderId()); if ($exporter === false) { throw new StatusException(sprintf("HandleSync() could not get an exporter for folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); } } catch (StatusException $stex) { $status = $stex->getCode(); } try { // Stream the messages directly to the PDA $streamimporter = new ImportChangesStream(self::$encoder, ZPush::getSyncObjectFromFolderClass($spa->GetContentClass())); if ($exporter !== false) { $exporter->Config($sc->GetParameter($spa, "state")); $exporter->ConfigContentParameters($spa->GetCPO()); $exporter->InitializeExporter($streamimporter); $changecount = $exporter->GetChangeCount(); } } catch (StatusException $stex) { if ($stex->getCode() === SYNC_FSSTATUS_CODEUNKNOWN && $spa->HasSyncKey()) { $status = SYNC_STATUS_INVALIDSYNCKEY; } else { $status = $stex->getCode(); } } if (!$spa->HasSyncKey()) { self::$topCollector->AnnounceInformation(sprintf("Exporter registered. %d objects queued.", $changecount), true); // update folder status as initialized $spa->SetFolderSyncTotal($changecount); $spa->SetFolderSyncRemaining($changecount); if ($changecount > 0) { self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_INITIALIZED); } } else { if ($status != SYNC_STATUS_SUCCESS) { self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), true); } } } } if (isset($hbinterval) && $changecount == 0 && $status == SYNC_STATUS_SUCCESS) { ZLog::Write(LOGLEVEL_DEBUG, "No changes found for heartbeat folder. Omitting empty output."); continue; } // Get a new sync key to output to the client if any changes have been send or will are available if (!empty($actiondata["modifyids"]) || !empty($actiondata["clientids"]) || !empty($actiondata["removeids"]) || $changecount > 0 || !$spa->HasSyncKey() && $status == SYNC_STATUS_SUCCESS) { $spa->SetNewSyncKey(self::$deviceManager->GetStateManager()->GetNewSyncKey($spa->GetSyncKey())); } self::$encoder->startTag(SYNC_FOLDER); if ($spa->HasContentClass()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Folder type: %s", $spa->GetContentClass())); // AS 12.0 devices require content class if (Request::GetProtocolVersion() < 12.1) { self::$encoder->startTag(SYNC_FOLDERTYPE); self::$encoder->content($spa->GetContentClass()); self::$encoder->endTag(); } } self::$encoder->startTag(SYNC_SYNCKEY); if ($status == SYNC_STATUS_SUCCESS && $spa->HasNewSyncKey()) { self::$encoder->content($spa->GetNewSyncKey()); } else { self::$encoder->content($spa->GetSyncKey()); } self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERID); self::$encoder->content($spa->GetFolderId()); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($status); self::$encoder->endTag(); // announce failing status to the process loop detection if ($status !== SYNC_STATUS_SUCCESS) { self::$deviceManager->AnnounceProcessStatus($spa->GetFolderId(), $status); } // Output IDs and status for incoming items & requests if ($status == SYNC_STATUS_SUCCESS && (!empty($actiondata["clientids"]) || !empty($actiondata["modifyids"]) || !empty($actiondata["removeids"]) || !empty($actiondata["fetchids"]))) { self::$encoder->startTag(SYNC_REPLIES); // output result of all new incoming items foreach ($actiondata["clientids"] as $clientid => $serverid) { self::$encoder->startTag(SYNC_ADD); self::$encoder->startTag(SYNC_CLIENTENTRYID); self::$encoder->content($clientid); self::$encoder->endTag(); if ($serverid) { self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); } self::$encoder->startTag(SYNC_STATUS); self::$encoder->content(isset($actiondata["statusids"][$clientid]) ? $actiondata["statusids"][$clientid] : SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR); self::$encoder->endTag(); self::$encoder->endTag(); } // loop through modify operations which were not a success, send status foreach ($actiondata["modifyids"] as $serverid) { if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_MODIFY); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($actiondata["statusids"][$serverid]); self::$encoder->endTag(); self::$encoder->endTag(); } } // loop through remove operations which were not a success, send status foreach ($actiondata["removeids"] as $serverid) { if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_REMOVE); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($actiondata["statusids"][$serverid]); self::$encoder->endTag(); self::$encoder->endTag(); } } if (!empty($actiondata["fetchids"])) { self::$topCollector->AnnounceInformation(sprintf("Fetching %d objects ", count($actiondata["fetchids"])), true); } foreach ($actiondata["fetchids"] as $id) { $data = false; try { $fetchstatus = SYNC_STATUS_SUCCESS; // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) { throw new StatusException(sprintf("HandleSync(): could not Setup() the backend to fetch in folder id '%s'", $spa->GetFolderId()), SYNC_STATUS_OBJECTNOTFOUND); } $data = self::$backend->Fetch($spa->GetFolderId(), $id, $spa->GetCPO()); // check if the message is broken if (ZPush::GetDeviceManager(false) && ZPush::GetDeviceManager()->DoNotStreamMessage($id, $data)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): message not to be streamed as requested by DeviceManager, id = %s", $id)); $fetchstatus = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; } } catch (StatusException $stex) { $fetchstatus = $stex->getCode(); } self::$encoder->startTag(SYNC_FETCH); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($id); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($fetchstatus); self::$encoder->endTag(); if ($data !== false && $status == SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_DATA); $data->Encode(self::$encoder); self::$encoder->endTag(); } else { ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to Fetch '%s'", $id)); } self::$encoder->endTag(); } self::$encoder->endTag(); } if ($sc->GetParameter($spa, "getchanges") && $spa->HasFolderId() && $spa->HasContentClass() && $spa->HasSyncKey()) { $windowSize = self::$deviceManager->GetWindowSize($spa->GetFolderId(), $spa->GetContentClass(), $spa->GetUuid(), $spa->GetUuidCounter(), $changecount); if ($changecount > $windowSize) { self::$encoder->startTag(SYNC_MOREAVAILABLE, false, true); } } // Stream outgoing changes if ($status == SYNC_STATUS_SUCCESS && $sc->GetParameter($spa, "getchanges") == true && $windowSize > 0) { self::$topCollector->AnnounceInformation(sprintf("Streaming data of %d objects", $changecount > $windowSize ? $windowSize : $changecount)); // Output message changes per folder self::$encoder->startTag(SYNC_PERFORM); $n = 0; while (1) { try { $progress = $exporter->Synchronize(); if (!is_array($progress)) { break; } $n++; } catch (SyncObjectBrokenException $mbe) { $brokenSO = $mbe->GetSyncObject(); if (!$brokenSO) { ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but broken SyncObject not available. This should be fixed in the backend.")); } else { if (!isset($brokenSO->id)) { $brokenSO->id = "Unknown ID"; ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but no ID of object set. This should be fixed in the backend.")); } self::$deviceManager->AnnounceIgnoredMessage($spa->GetFolderId(), $brokenSO->id, $brokenSO); } } catch (StatusException $stex) { $status = $stex->getCode(); // during export we found out that the states should be thrown away (ZP-623) if ($status == SYNC_STATUS_INVALIDSYNCKEY) { self::$deviceManager->ForceFolderResync($spa->GetFolderId()); break; } } if ($n >= $windowSize) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Exported maxItems of messages: %d / %d", $n, $changecount)); break; } } // $progress is not an array when exporting the last message // so we get the number to display from the streamimporter if (isset($streamimporter)) { $n = $streamimporter->GetImportedMessages(); } self::$encoder->endTag(); self::$topCollector->AnnounceInformation(sprintf("Outgoing %d objects%s", $n, $n >= $windowSize ? " of " . $changecount : ""), true); // update folder status, if there is something set if ($spa->GetFolderSyncRemaining() && $changecount > 0) { $spa->SetFolderSyncRemaining($changecount); } // changecount is initialized with 'false', so 0 means no changes! if ($changecount === 0 || $changecount !== false && $changecount <= $windowSize) { self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_COMPLETED); } else { self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_INPROGRESS); } } self::$encoder->endTag(); // Save the sync state for the next time if ($spa->HasNewSyncKey()) { self::$topCollector->AnnounceInformation("Saving state"); try { if (isset($exporter) && $exporter) { $state = $exporter->GetState(); } else { if ($sc->GetParameter($spa, "state") !== null) { $state = $sc->GetParameter($spa, "state"); } else { if (!$spa->HasSyncKey()) { $state = ""; } } } } catch (StatusException $stex) { $status = $stex->getCode(); } if (isset($state) && $status == SYNC_STATUS_SUCCESS) { self::$deviceManager->GetStateManager()->SetSyncState($spa->GetNewSyncKey(), $state, $spa->GetFolderId()); } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): error saving '%s' - no state information available", $spa->GetNewSyncKey())); } } // save SyncParameters if ($status == SYNC_STATUS_SUCCESS && empty($actiondata["fetchids"])) { $sc->SaveCollection($spa); } // reset status for the next folder $status = SYNC_STATUS_SUCCESS; } // END foreach collection self::$encoder->endTag(); //SYNC_FOLDERS } self::$encoder->endTag(); //SYNC_SYNCHRONIZE return true; }
/** * Returns the timestamp of the last synchronization of a device. * * @param $device an ASDevice * * @access public * @return int timestamp */ public static function GetLastSyncTimeOfDevice(&$device) { // we need a StateManager for this operation $stateManager = new StateManager(); $stateManager->SetDevice($device); $sc = new SyncCollections(); $sc->SetStateManager($stateManager); // load all collections of device without loading states or checking permissions $sc->LoadAllCollections(true, false, false); return $sc->GetLastSyncTime(); }
/** * Synchronizes a folder to the output stream. Changes for this folders are expected. * * @param SyncCollections $sc * @param SyncParameters $spa * @param IExportChanges $exporter Fully configured exporter for this folder * @param int $changecount Amount of changes expected * @param ImportChangesStream $streamimporter Output stream * @param int $status current status of the folder processing * @param string $newFolderStat the new folder stat to be set if everything was exported * * @throws StatusException * @return int sync status code */ private function syncFolder($sc, $spa, $exporter, $changecount, $streamimporter, $status, $newFolderStat) { $actiondata = $sc->GetParameter($spa, "actiondata"); // send the WBXML start tags (if not happened already) $this->sendFolderStartTag(); self::$encoder->startTag(SYNC_FOLDER); if ($spa->HasContentClass()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Folder type: %s", $spa->GetContentClass())); // AS 12.0 devices require content class if (Request::GetProtocolVersion() < 12.1) { self::$encoder->startTag(SYNC_FOLDERTYPE); self::$encoder->content($spa->GetContentClass()); self::$encoder->endTag(); } } self::$encoder->startTag(SYNC_SYNCKEY); if ($status == SYNC_STATUS_SUCCESS && $spa->HasNewSyncKey()) { self::$encoder->content($spa->GetNewSyncKey()); } else { self::$encoder->content($spa->GetSyncKey()); } self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERID); self::$encoder->content($spa->GetFolderId()); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($status); self::$encoder->endTag(); // announce failing status to the process loop detection if ($status !== SYNC_STATUS_SUCCESS) { self::$deviceManager->AnnounceProcessStatus($spa->GetFolderId(), $status); } // Output IDs and status for incoming items & requests if ($status == SYNC_STATUS_SUCCESS && (!empty($actiondata["clientids"]) || !empty($actiondata["modifyids"]) || !empty($actiondata["removeids"]) || !empty($actiondata["fetchids"]))) { self::$encoder->startTag(SYNC_REPLIES); // output result of all new incoming items foreach ($actiondata["clientids"] as $clientid => $serverid) { self::$encoder->startTag(SYNC_ADD); self::$encoder->startTag(SYNC_CLIENTENTRYID); self::$encoder->content($clientid); self::$encoder->endTag(); if ($serverid) { self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); } self::$encoder->startTag(SYNC_STATUS); self::$encoder->content(isset($actiondata["statusids"][$clientid]) ? $actiondata["statusids"][$clientid] : SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR); self::$encoder->endTag(); self::$encoder->endTag(); } // loop through modify operations which were not a success, send status foreach ($actiondata["modifyids"] as $serverid) { if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_MODIFY); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($actiondata["statusids"][$serverid]); self::$encoder->endTag(); self::$encoder->endTag(); } } // loop through remove operations which were not a success, send status foreach ($actiondata["removeids"] as $serverid) { if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_REMOVE); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($actiondata["statusids"][$serverid]); self::$encoder->endTag(); self::$encoder->endTag(); } } if (!empty($actiondata["fetchids"])) { self::$topCollector->AnnounceInformation(sprintf("Fetching %d objects ", count($actiondata["fetchids"])), $this->singleFolder); $this->saveMultiFolderInfo("fetching", count($actiondata["fetchids"])); } foreach ($actiondata["fetchids"] as $id) { $data = false; try { $fetchstatus = SYNC_STATUS_SUCCESS; // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()))) { throw new StatusException(sprintf("HandleSync(): could not Setup() the backend to fetch in folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), SYNC_STATUS_OBJECTNOTFOUND); } $data = self::$backend->Fetch($spa->GetBackendFolderId(), $id, $spa->GetCPO()); // check if the message is broken if (ZPush::GetDeviceManager(false) && ZPush::GetDeviceManager()->DoNotStreamMessage($id, $data)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): message not to be streamed as requested by DeviceManager, id = %s", $id)); $fetchstatus = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; } } catch (StatusException $stex) { $fetchstatus = $stex->getCode(); } self::$encoder->startTag(SYNC_FETCH); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($id); self::$encoder->endTag(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($fetchstatus); self::$encoder->endTag(); if ($data !== false && $status == SYNC_STATUS_SUCCESS) { self::$encoder->startTag(SYNC_DATA); $data->Encode(self::$encoder); self::$encoder->endTag(); } else { ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to Fetch '%s'", $id)); } self::$encoder->endTag(); } self::$encoder->endTag(); } if ($sc->GetParameter($spa, "getchanges") && $spa->HasFolderId() && $spa->HasContentClass() && $spa->HasSyncKey()) { $moreAvailableSent = false; $windowSize = self::$deviceManager->GetWindowSize($spa->GetFolderId(), $spa->GetUuid(), $spa->GetUuidCounter(), $changecount); // limit windowSize to the max available limit of the global window size left $globallyAvailable = $sc->GetGlobalWindowSize() - $this->globallyExportedItems; if ($changecount > $globallyAvailable && $windowSize > $globallyAvailable) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Limit window size to %d as the global window size limit will be reached", $globallyAvailable)); $windowSize = $globallyAvailable; } // send <MoreAvailable/> if there are more changes than fit in the folder windowsize // or there is a move state (another sync should be done afterwards) if ($changecount > $windowSize || $spa->GetMoveState() !== false) { self::$encoder->startTag(SYNC_MOREAVAILABLE, false, true); $moreAvailableSent = true; $spa->DelFolderStat(); } } // Stream outgoing changes if ($status == SYNC_STATUS_SUCCESS && $sc->GetParameter($spa, "getchanges") == true && $windowSize > 0 && !!$exporter) { self::$topCollector->AnnounceInformation(sprintf("Streaming data of %d objects", $changecount > $windowSize ? $windowSize : $changecount)); // Output message changes per folder self::$encoder->startTag(SYNC_PERFORM); $n = 0; WBXMLDecoder::ResetInWhile("syncSynchronize"); while (WBXMLDecoder::InWhile("syncSynchronize")) { try { $progress = $exporter->Synchronize(); if (!is_array($progress)) { break; } $n++; if ($n % 10 == 0) { self::$topCollector->AnnounceInformation(sprintf("Streamed data of %d objects out of %d", $n, $changecount > $windowSize ? $windowSize : $changecount)); } } catch (SyncObjectBrokenException $mbe) { $brokenSO = $mbe->GetSyncObject(); if (!$brokenSO) { ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but broken SyncObject not available. This should be fixed in the backend.")); } else { if (!isset($brokenSO->id)) { $brokenSO->id = "Unknown ID"; ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): Catched SyncObjectBrokenException but no ID of object set. This should be fixed in the backend.")); } self::$deviceManager->AnnounceIgnoredMessage($spa->GetFolderId(), $brokenSO->id, $brokenSO); } } catch (StatusException $stex) { $status = $stex->getCode(); // during export we found out that the states should be thrown away (ZP-623) if ($status == SYNC_STATUS_INVALIDSYNCKEY) { self::$deviceManager->ForceFolderResync($spa->GetFolderId()); break; } } if ($n >= $windowSize || Request::IsRequestTimeoutReached()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): Exported maxItems of messages: %d / %d", $n, $changecount)); break; } } // $progress is not an array when exporting the last message // so we get the number to display from the streamimporter if it's available if (!!$streamimporter) { $n = $streamimporter->GetImportedMessages(); } self::$encoder->endTag(); // log the request timeout if (Request::IsRequestTimeoutReached()) { ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): Stopping export as maximum request timeout is almost reached!"); // Send a <MoreAvailable/> tag if we reached the request timeout, there are more changes and a moreavailable was not already send if (!$moreAvailableSent && $n > $windowSize) { self::$encoder->startTag(SYNC_MOREAVAILABLE, false, true); $spa->DelFolderStat(); $moreAvailableSent = true; } } self::$topCollector->AnnounceInformation(sprintf("Outgoing %d objects%s", $n, $n >= $windowSize ? " of " . $changecount : ""), $this->singleFolder); $this->saveMultiFolderInfo("outgoing", $n); $this->saveMultiFolderInfo("queued", $changecount); $this->globallyExportedItems += $n; // update folder status, if there is something set if ($spa->GetFolderSyncRemaining() && $changecount > 0) { $spa->SetFolderSyncRemaining($changecount); } // changecount is initialized with 'false', so 0 means no changes! if ($changecount === 0 || $changecount !== false && $changecount <= $windowSize) { self::$deviceManager->SetFolderSyncStatus($spa->GetFolderId(), DeviceManager::FLD_SYNC_COMPLETED); // we should update the folderstat, but we recheck to see if it changed. If so, it's not updated to force another sync $newFolderStatAfterExport = self::$backend->GetFolderStat(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()), $spa->GetBackendFolderId()); if ($newFolderStat === $newFolderStatAfterExport) { $this->setFolderStat($spa, $newFolderStat); } else { ZLog::Write(LOGLEVEL_DEBUG, "Sync() Folderstat differs after export, force another exporter run."); } } else { self::$deviceManager->SetFolderSyncStatus($spa->GetFolderId(), DeviceManager::FLD_SYNC_INPROGRESS); } } self::$encoder->endTag(); // Save the sync state for the next time if ($spa->HasNewSyncKey()) { self::$topCollector->AnnounceInformation("Saving state"); try { if (isset($exporter) && $exporter) { $state = $exporter->GetState(); // update the move state (it should be gone now) list($moveState, ) = $exporter->GetMoveStates(); $spa->SetMoveState($moveState); } else { if ($sc->GetParameter($spa, "state") !== null) { $state = $sc->GetParameter($spa, "state"); } else { if (!$spa->HasSyncKey()) { $state = ""; } } } } catch (StatusException $stex) { $status = $stex->getCode(); } if (isset($state) && $status == SYNC_STATUS_SUCCESS) { self::$deviceManager->GetStateManager()->SetSyncState($spa->GetNewSyncKey(), $state, $spa->GetFolderId()); } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("HandleSync(): error saving '%s' - no state information available", $spa->GetNewSyncKey())); } } // save SyncParameters if ($status == SYNC_STATUS_SUCCESS && empty($actiondata["fetchids"])) { $sc->SaveCollection($spa); } return $status; }
/** * Handles the GetItemEstimate command * Returns an estimation of how many items will be synchronized at the next sync * This is mostly used to show something in the progress bar * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { $sc = new SyncCollections(); if (!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE)) { return false; } if (!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERS)) { return false; } while (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDER)) { $spa = new SyncParameters(); $spastatus = false; if (Request::GetProtocolVersion() >= 14.0) { if (self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { try { $spa->SetSyncKey(self::$decoder->getElementContent()); } catch (StateInvalidException $siex) { $spastatus = SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED; } if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) { $spa->SetFolderId(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } // conversation mode requested if (self::$decoder->getElementStartTag(SYNC_CONVERSATIONMODE)) { $spa->SetConversationMode(true); if (($conversationmode = self::$decoder->getElementContent()) !== false) { $spa->SetConversationMode((bool) $conversationmode); if (!self::$decoder->getElementEndTag()) { return false; } } } if (self::$decoder->getElementStartTag(SYNC_OPTIONS)) { while (1) { if (self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { $spa->SetFilterType(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { $spa->SetContentClass(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_MAXITEMS)) { $spa->SetWindowSize($maxitems = self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } $e = self::$decoder->peek(); if ($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); break; } } } } else { //get items estimate does not necessarily send the folder type if (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERTYPE)) { $spa->SetContentClass(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) { $spa->SetFolderId(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (!self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { return false; } $spa->SetFilterType(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } if (!self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { return false; } try { $spa->SetSyncKey(self::$decoder->getElementContent()); } catch (StateInvalidException $siex) { $spastatus = SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED; } if (!self::$decoder->getElementEndTag()) { return false; } } if (!self::$decoder->getElementEndTag()) { return false; } //SYNC_GETITEMESTIMATE_FOLDER // Process folder data //In AS 14 request only collectionid is sent, without class if (!$spa->HasContentClass() && $spa->HasFolderId()) { try { $spa->SetContentClass(self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId())); } catch (NoHierarchyCacheAvailableException $nhca) { $spastatus = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; } } // compatibility mode AS 1.0 - get folderid which was sent during GetHierarchy() if (!$spa->HasFolderId() && $spa->HasContentClass()) { $spa->SetFolderId(self::$deviceManager->GetFolderIdFromCacheByClass($spa->GetContentClass())); } // Add collection to SC and load state $sc->AddCollection($spa); if ($spastatus) { // the CPO has a folder id now, so we can set the status $sc->AddParameter($spa, "status", $spastatus); } else { try { $sc->AddParameter($spa, "state", self::$deviceManager->GetStateManager()->GetSyncState($spa->GetSyncKey())); // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetFolderId()))) { throw new StatusException(sprintf("HandleGetItemEstimate() could not Setup() the backend for folder id '%s'", $spa->GetFolderId()), SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } } catch (StateNotFoundException $snfex) { // ok, the key is invalid. Question is, if the hierarchycache is still ok //if not, we have to issue SYNC_GETITEMESTSTATUS_COLLECTIONINVALID which triggers a FolderSync try { self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId()); // we got here, so the HierarchyCache is ok $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCKKEYINVALID); } catch (NoHierarchyCacheAvailableException $nhca) { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } self::$topCollector->AnnounceInformation("StateNotFoundException " . $sc->GetParameter($spa, "status"), true); } catch (StatusException $stex) { if ($stex->getCode() == SYNC_GETITEMESTSTATUS_COLLECTIONINVALID) { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } else { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED); } self::$topCollector->AnnounceInformation("StatusException " . $sc->GetParameter($spa, "status"), true); } } } if (!self::$decoder->getElementEndTag()) { return false; } //SYNC_GETITEMESTIMATE_FOLDERS if (!self::$decoder->getElementEndTag()) { return false; } //SYNC_GETITEMESTIMATE_GETITEMESTIMATE self::$encoder->startWBXML(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE); $status = SYNC_GETITEMESTSTATUS_SUCCESS; // look for changes in all collections try { $sc->CountChanges(); } catch (StatusException $ste) { $status = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; } $changes = $sc->GetChangedFolderIds(); foreach ($sc as $folderid => $spa) { self::$encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE); if ($sc->GetParameter($spa, "status")) { $status = $sc->GetParameter($spa, "status"); } self::$encoder->startTag(SYNC_GETITEMESTIMATE_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE); self::$encoder->content($spa->GetContentClass()); self::$encoder->endTag(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID); self::$encoder->content($spa->GetFolderId()); self::$encoder->endTag(); if (isset($changes[$folderid]) && $changes[$folderid] !== false) { self::$encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE); self::$encoder->content($changes[$folderid]); self::$encoder->endTag(); if ($changes[$folderid] > 0) { self::$topCollector->AnnounceInformation(sprintf("%s %d changes", $spa->GetContentClass(), $changes[$folderid]), true); } } self::$encoder->endTag(); self::$encoder->endTag(); } if (array_sum($changes) == 0) { self::$topCollector->AnnounceInformation("No changes found", true); } self::$encoder->endTag(); return true; }
/** * Returns details of a device like synctimes, * policy and wipe status, synched folders etc * * @param string $devid device id * @param string $user user to be looked up * * @return ASDevice object * @access public */ public static function GetDeviceDetails($devid, $user) { try { $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); $device->SetData(ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA), false); $device->StripData(); try { $lastsync = SyncCollections::GetLastSyncTimeOfDevice($device); if ($lastsync) { $device->SetLastSyncTime($lastsync); } } catch (StateInvalidException $sive) { ZLog::Write(LOGLEVEL_WARN, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' has invalid states. Please sync to solve this issue.", $devid, $user)); $device->SetDeviceError("Invalid states. Please force synchronization!"); } return $device; } catch (StateNotFoundException $e) { ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' can not be found", $devid, $user)); return false; } }
/** * Returns details of a device like synctimes, * policy and wipe status, synched folders etc * * @param string $devid device id * @param string $user user to be looked up * * @return ASDevice object * @access public */ public static function GetDeviceDetails($devid, $user) { try { $device = new ASDevice($devid, ASDevice::UNDEFINED, $user, ASDevice::UNDEFINED); $device->SetData(ZPush::GetStateMachine()->GetState($devid, IStateMachine::DEVICEDATA), false); $device->StripData(); try { // we need a StateManager for this operation $stateManager = new StateManager(); $stateManager->SetDevice($device); $sc = new SyncCollections(); $sc->SetStateManager($stateManager); // load all collections of device without loading states or checking permissions $sc->LoadAllCollections(true, false, false); if ($sc->GetLastSyncTime()) { $device->SetLastSyncTime($sc->GetLastSyncTime()); } // get information about the folder synchronization status from SyncCollections $folders = $device->GetAllFolderIds(); foreach ($folders as $folderid) { $fstatus = $device->GetFolderSyncStatus($folderid); if ($fstatus !== false && isset($fstatus[ASDevice::FOLDERSYNCSTATUS])) { $spa = $sc->GetCollection($folderid); $total = $spa->GetFolderSyncTotal(); $todo = $spa->GetFolderSyncRemaining(); $fstatus['status'] = $fstatus[ASDevice::FOLDERSYNCSTATUS] == 1 ? 'Initialized' : 'Synchronizing'; $fstatus['total'] = $total; $fstatus['done'] = $total - $todo; $fstatus['todo'] = $todo; $device->SetFolderSyncStatus($folderid, $fstatus); } } } catch (StateInvalidException $sive) { ZLog::Write(LOGLEVEL_WARN, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' has invalid states. Please sync to solve this issue.", $devid, $user)); $device->SetDeviceError("Invalid states. Please force synchronization!"); } return $device; } catch (StateNotFoundException $e) { ZLog::Write(LOGLEVEL_ERROR, sprintf("ZPushAdmin::GetDeviceDetails(): device '%s' of user '%s' can not be found", $devid, $user)); return false; } }
/** * Handles creates, updates or deletes of a folder * issued by the commands FolderCreate, FolderUpdate and FolderDelete * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { $el = self::$decoder->getElement(); if ($el[EN_TYPE] != EN_TYPE_STARTTAG) { return false; } $create = $update = $delete = false; if ($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERCREATE) { $create = true; } else { if ($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERUPDATE) { $update = true; } else { if ($el[EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERDELETE) { $delete = true; } } } if (!$create && !$update && !$delete) { return false; } // SyncKey if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) { return false; } $synckey = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } // ServerID $serverid = false; $backendid = false; if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID)) { $serverid = self::$decoder->getElementContent(); $backendid = self::$deviceManager->GetBackendIdForFolderId($serverid); if (!self::$decoder->getElementEndTag()) { return false; } } // Parent $parentid = false; $parentBackendId = false; // when creating or updating more information is necessary if (!$delete) { if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_PARENTID)) { $parentid = self::$decoder->getElementContent(); $parentBackendId = self::$deviceManager->GetBackendIdForFolderId($parentid); if (!self::$decoder->getElementEndTag()) { return false; } } // Displayname if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_DISPLAYNAME)) { return false; } $displayname = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } // Type $type = false; if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_TYPE)) { $type = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } } } // endtag foldercreate, folderupdate, folderdelete if (!self::$decoder->getElementEndTag()) { return false; } $status = SYNC_FSSTATUS_SUCCESS; // Get state of hierarchy try { $syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey); $newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey); // there are no SyncParameters for the hierarchy, but we use it to save the latest synckeys $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState(false); // Over the ChangesWrapper the HierarchyCache is notified about all changes $changesMem = self::$deviceManager->GetHierarchyChangesWrapper(); // the hierarchyCache should now fully be initialized - check for changes in the additional folders $changesMem->Config(ZPush::GetAdditionalSyncFolders(false)); // reset to default store in backend self::$backend->Setup(false); // there are unprocessed changes in the hierarchy, trigger resync if ($changesMem->GetChangeCount() > 0) { throw new StatusException("HandleFolderChange() can not proceed as there are unprocessed hierarchy changes", SYNC_FSSTATUS_SERVERERROR); } // any additional folders can not be modified! if ($serverid !== false && ZPush::GetAdditionalSyncFolderStore($backendid)) { throw new StatusException("HandleFolderChange() can not change additional folders which are configured", SYNC_FSSTATUS_SYSTEMFOLDER); } // switch user store if this this happens inside an additional folder // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($parentBackendId != false ? $parentBackendId : $backendid))) { throw new StatusException(sprintf("HandleFolderChange() could not Setup() the backend for folder id '%s'", $parentBackendId != false ? $parentBackendId : $backendid), SYNC_FSSTATUS_SERVERERROR); } } catch (StateNotFoundException $snfex) { $status = SYNC_FSSTATUS_SYNCKEYERROR; } catch (StatusException $stex) { $status = $stex->getCode(); } // set $newsynckey in case of an error if (!isset($newsynckey)) { $newsynckey = $synckey; } if ($status == SYNC_FSSTATUS_SUCCESS) { try { // Configure importer with last state $importer = self::$backend->GetImporter(); $importer->Config($syncstate); // the messages from the PIM will be forwarded to the real importer $changesMem->SetDestinationImporter($importer); // Create SyncFolder object $folder = new SyncFolder(); $folder->serverid = $serverid; $folder->parentid = $parentBackendId; if (isset($displayname)) { $folder->displayname = $displayname; } if (isset($type)) { $folder->type = $type; } // add the backendId to the SyncFolder object $folder->BackendId = $backendid; // process incoming change if (!$delete) { // when creating, $folder->serverid is false, and the returned id is already mapped by the backend $folder = $changesMem->ImportFolderChange($folder); } else { // delete folder $changesMem->ImportFolderDeletion($folder); } } catch (StatusException $stex) { $status = $stex->getCode(); } } self::$encoder->startWBXML(); if ($create) { self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERCREATE); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); self::$encoder->content($newsynckey); self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID); self::$encoder->content($folder->serverid); self::$encoder->endTag(); self::$encoder->endTag(); } elseif ($update) { self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERUPDATE); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); self::$encoder->content($newsynckey); self::$encoder->endTag(); self::$encoder->endTag(); } elseif ($delete) { self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERDELETE); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); self::$encoder->content($newsynckey); self::$encoder->endTag(); self::$encoder->endTag(); } self::$topCollector->AnnounceInformation(sprintf("Operation status %d", $status), true); // Save the sync state for the next time if (isset($importer)) { self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $importer->GetState()); // update SPA & save it $spa->SetSyncKey($newsynckey); $spa->SetFolderId(false); self::$deviceManager->GetStateManager()->SetSynchedFolderState($spa); // invalidate all pingable flags SyncCollections::InvalidatePingableFlags(); } return true; }
/** * Handles the GetItemEstimate command * Returns an estimation of how many items will be synchronized at the next sync * This is mostly used to show something in the progress bar * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { $sc = new SyncCollections(); if (!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE)) { return false; } if (!self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERS)) { return false; } while (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDER)) { $spa = new SyncParameters(); $spastatus = false; // read the folder properties WBXMLDecoder::ResetInWhile("getItemEstimateFolders"); while (WBXMLDecoder::InWhile("getItemEstimateFolders")) { if (self::$decoder->getElementStartTag(SYNC_SYNCKEY)) { try { $spa->SetSyncKey(self::$decoder->getElementContent()); } catch (StateInvalidException $siex) { $spastatus = SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED; } if (!self::$decoder->getElementEndTag()) { return false; } } elseif (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) { $fid = self::$decoder->getElementContent(); $spa->SetFolderId($fid); $spa->SetBackendFolderId(self::$deviceManager->GetBackendIdForFolderId($fid)); if (!self::$decoder->getElementEndTag()) { return false; } } elseif (self::$decoder->getElementStartTag(SYNC_CONVERSATIONMODE)) { $spa->SetConversationMode(true); if (($conversationmode = self::$decoder->getElementContent()) !== false) { $spa->SetConversationMode((bool) $conversationmode); if (!self::$decoder->getElementEndTag()) { return false; } } } elseif (self::$decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERTYPE)) { $spa->SetContentClass(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } elseif (self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { $spa->SetFilterType(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } while (self::$decoder->getElementStartTag(SYNC_OPTIONS)) { WBXMLDecoder::ResetInWhile("getItemEstimateOptions"); while (WBXMLDecoder::InWhile("getItemEstimateOptions")) { $firstOption = true; // foldertype definition if (self::$decoder->getElementStartTag(SYNC_FOLDERTYPE)) { $foldertype = self::$decoder->getElementContent(); ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleGetItemEstimate(): specified options block with foldertype '%s'", $foldertype)); // switch the foldertype for the next options $spa->UseCPO($foldertype); // set to synchronize all changes. The mobile could overwrite this value $spa->SetFilterType(SYNC_FILTERTYPE_ALL); if (!self::$decoder->getElementEndTag()) { return false; } } else { if ($firstOption) { $spa->UseCPO(); // set to synchronize all changes. The mobile could overwrite this value $spa->SetFilterType(SYNC_FILTERTYPE_ALL); } } $firstOption = false; if (self::$decoder->getElementStartTag(SYNC_FILTERTYPE)) { $spa->SetFilterType(self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } if (self::$decoder->getElementStartTag(SYNC_MAXITEMS)) { $spa->SetWindowSize($maxitems = self::$decoder->getElementContent()); if (!self::$decoder->getElementEndTag()) { return false; } } $e = self::$decoder->peek(); if ($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); break; } } } $e = self::$decoder->peek(); if ($e[EN_TYPE] == EN_TYPE_ENDTAG) { self::$decoder->getElementEndTag(); //SYNC_GETITEMESTIMATE_FOLDER break; } } // Process folder data //In AS 14 request only collectionid is sent, without class if (!$spa->HasContentClass() && $spa->HasFolderId()) { try { $spa->SetContentClass(self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId())); } catch (NoHierarchyCacheAvailableException $nhca) { $spastatus = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; } } // compatibility mode AS 1.0 - get folderid which was sent during GetHierarchy() if (!$spa->HasFolderId() && $spa->HasContentClass()) { $spa->SetFolderId(self::$deviceManager->GetFolderIdFromCacheByClass($spa->GetContentClass())); } // Add collection to SC and load state $sc->AddCollection($spa); if ($spastatus) { // the CPO has a folder id now, so we can set the status $sc->AddParameter($spa, "status", $spastatus); } else { try { $sc->AddParameter($spa, "state", self::$deviceManager->GetStateManager()->GetSyncState($spa->GetSyncKey())); // if this is an additional folder the backend has to be setup correctly if (!self::$backend->Setup(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()))) { throw new StatusException(sprintf("HandleGetItemEstimate() could not Setup() the backend for folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } } catch (StateNotFoundException $snfex) { // ok, the key is invalid. Question is, if the hierarchycache is still ok //if not, we have to issue SYNC_GETITEMESTSTATUS_COLLECTIONINVALID which triggers a FolderSync try { self::$deviceManager->GetFolderClassFromCacheByID($spa->GetFolderId()); // we got here, so the HierarchyCache is ok $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCKKEYINVALID); } catch (NoHierarchyCacheAvailableException $nhca) { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } self::$topCollector->AnnounceInformation("StateNotFoundException " . $sc->GetParameter($spa, "status"), true); } catch (StatusException $stex) { if ($stex->getCode() == SYNC_GETITEMESTSTATUS_COLLECTIONINVALID) { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_COLLECTIONINVALID); } else { $sc->AddParameter($spa, "status", SYNC_GETITEMESTSTATUS_SYNCSTATENOTPRIMED); } self::$topCollector->AnnounceInformation("StatusException " . $sc->GetParameter($spa, "status"), true); } } } if (!self::$decoder->getElementEndTag()) { return false; } //SYNC_GETITEMESTIMATE_FOLDERS if (!self::$decoder->getElementEndTag()) { return false; } //SYNC_GETITEMESTIMATE_GETITEMESTIMATE self::$encoder->startWBXML(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE); $status = SYNC_GETITEMESTSTATUS_SUCCESS; // look for changes in all collections try { $sc->CountChanges(); } catch (StatusException $ste) { $status = SYNC_GETITEMESTSTATUS_COLLECTIONINVALID; } $changes = $sc->GetChangedFolderIds(); foreach ($sc as $folderid => $spa) { self::$encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE); if ($sc->GetParameter($spa, "status")) { $status = $sc->GetParameter($spa, "status"); } self::$encoder->startTag(SYNC_GETITEMESTIMATE_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE); self::$encoder->content($spa->GetContentClass()); self::$encoder->endTag(); self::$encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID); self::$encoder->content($spa->GetFolderId()); self::$encoder->endTag(); if (isset($changes[$folderid]) && $changes[$folderid] !== false) { self::$encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE); self::$encoder->content($changes[$folderid]); self::$encoder->endTag(); if ($changes[$folderid] > 0) { self::$topCollector->AnnounceInformation(sprintf("%s %d changes", $spa->GetContentClass(), $changes[$folderid]), true); } // update the device data to mark folders as complete when synching with WM if ($changes[$folderid] == 0) { self::$deviceManager->SetFolderSyncStatus($folderid, DeviceManager::FLD_SYNC_COMPLETED); } } self::$encoder->endTag(); self::$encoder->endTag(); } if (array_sum($changes) == 0) { self::$topCollector->AnnounceInformation("No changes found", true); } self::$encoder->endTag(); return true; }
/** * Handles the FolderSync command * * @param int $commandCode * * @access public * @return boolean */ public function Handle($commandCode) { // Parse input if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) { return false; } if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) { return false; } $synckey = self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } // every FolderSync with SyncKey 0 should return the supported AS version & command headers if ($synckey == "0") { self::$specialHeaders = array(); self::$specialHeaders[] = ZPush::GetSupportedProtocolVersions(); self::$specialHeaders[] = ZPush::GetSupportedCommands(); } $status = SYNC_FSSTATUS_SUCCESS; $newsynckey = $synckey; try { $syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey); // We will be saving the sync state under 'newsynckey' $newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey); // there are no SyncParameters for the hierarchy, but we use it to save the latest synckeys $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState(false); } catch (StateNotFoundException $snfex) { $status = SYNC_FSSTATUS_SYNCKEYERROR; } catch (StateInvalidException $sive) { $status = SYNC_FSSTATUS_SYNCKEYERROR; } // The ChangesWrapper caches all imports in-memory, so we can send a change count // before sending the actual data. // the HierarchyCache is notified and the changes from the PIM are transmitted to the actual backend $changesMem = self::$deviceManager->GetHierarchyChangesWrapper(); // the hierarchyCache should now fully be initialized - check for changes in the additional folders $changesMem->Config(ZPush::GetAdditionalSyncFolders(false)); // reset to default store in backend self::$backend->Setup(false); // process incoming changes if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) { // Ignore <Count> if present if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) { self::$decoder->getElementContent(); if (!self::$decoder->getElementEndTag()) { return false; } } // Process the changes (either <Add>, <Modify>, or <Remove>) $element = self::$decoder->getElement(); if ($element[EN_TYPE] != EN_TYPE_STARTTAG) { return false; } $importer = false; WBXMLDecoder::ResetInWhile("folderSyncIncomingChange"); while (WBXMLDecoder::InWhile("folderSyncIncomingChange")) { $folder = new SyncFolder(); if (!$folder->Decode(self::$decoder)) { break; } // add the backendId to the SyncFolder object $folder->BackendId = self::$deviceManager->GetBackendIdForFolderId($folder->serverid); try { if ($status == SYNC_FSSTATUS_SUCCESS && !$importer) { // Configure the backends importer with last state $importer = self::$backend->GetImporter(); $importer->Config($syncstate); // the messages from the PIM will be forwarded to the backend $changesMem->forwardImporter($importer); } if ($status == SYNC_FSSTATUS_SUCCESS) { switch ($element[EN_TAG]) { case SYNC_ADD: case SYNC_MODIFY: $serverid = $changesMem->ImportFolderChange($folder); break; case SYNC_REMOVE: $serverid = $changesMem->ImportFolderDeletion($folder); break; } } else { ZLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): ignoring incoming folderchange for folder '%s' as status indicates problem.", $folder->displayname)); self::$topCollector->AnnounceInformation("Incoming change ignored", true); } } catch (StatusException $stex) { $status = $stex->getCode(); } } if (!self::$decoder->getElementEndTag()) { return false; } } else { // check for a potential process loop like described in Issue ZP-5 if ($synckey != "0" && self::$deviceManager->IsHierarchyFullResyncRequired()) { $status = SYNC_FSSTATUS_SYNCKEYERROR; } self::$deviceManager->AnnounceProcessStatus(false, $status); } if (!self::$decoder->getElementEndTag()) { return false; } // We have processed incoming foldersync requests, now send the PIM // our changes // Output our WBXML reply now self::$encoder->StartWBXML(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC); if ($status == SYNC_FSSTATUS_SUCCESS) { try { // do nothing if this is an invalid device id (like the 'validate' Androids internal client sends) if (!Request::IsValidDeviceID()) { throw new StatusException(sprintf("Request::IsValidDeviceID() indicated that '%s' is not a valid device id", Request::GetDeviceID()), SYNC_FSSTATUS_SERVERERROR); } // Changes from backend are sent to the MemImporter and processed for the HierarchyCache. // The state which is saved is from the backend, as the MemImporter is only a proxy. $exporter = self::$backend->GetExporter(); $exporter->Config($syncstate); $exporter->InitializeExporter($changesMem); // Stream all changes to the ImportExportChangesMem $totalChanges = $exporter->GetChangeCount(); $exported = 0; $partial = false; while (is_array($exporter->Synchronize())) { $exported++; if (time() % 4) { self::$topCollector->AnnounceInformation(sprintf("Exported %d from %d folders", $exported, $totalChanges)); } // if partial sync is allowed, stop if this takes too long if (USE_PARTIAL_FOLDERSYNC && Request::IsRequestTimeoutReached()) { ZLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): Exporting folders is too slow. In %d seconds only %d from %d changes were processed.", time() - $_SERVER["REQUEST_TIME"], $exported, $totalChanges)); self::$topCollector->AnnounceInformation(sprintf("Partial export of %d out of %d folders", $exported, $totalChanges), true); self::$deviceManager->SetFolderSyncComplete(false); $partial = true; break; } } // update the foldersync complete flag if (USE_PARTIAL_FOLDERSYNC && $partial == false && self::$deviceManager->GetFolderSyncComplete() === false) { // say that we are done with partial synching self::$deviceManager->SetFolderSyncComplete(true); // reset the loop data to prevent any loop detection to kick in now self::$deviceManager->ClearLoopDetectionData(Request::GetAuthUser(), Request::GetDeviceID()); ZLog::Write(LOGLEVEL_INFO, "Request->HandleFolderSync(): Chunked exporting of folders completed successfully"); } // get the new state from the backend $newsyncstate = isset($exporter) ? $exporter->GetState() : ""; } catch (StatusException $stex) { if ($stex->getCode() == SYNC_FSSTATUS_CODEUNKNOWN) { $status = SYNC_FSSTATUS_SYNCKEYERROR; } else { $status = $stex->getCode(); } } } self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); self::$encoder->content($status); self::$encoder->endTag(); if ($status == SYNC_FSSTATUS_SUCCESS) { self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); $synckey = $changesMem->IsStateChanged() ? $newsynckey : $synckey; self::$encoder->content($synckey); self::$encoder->endTag(); // Stream folders directly to the PDA $streamimporter = new ImportChangesStream(self::$encoder, false); $changesMem->InitializeExporter($streamimporter); $changeCount = $changesMem->GetChangeCount(); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES); self::$encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT); self::$encoder->content($changeCount); self::$encoder->endTag(); while ($changesMem->Synchronize()) { } self::$encoder->endTag(); self::$topCollector->AnnounceInformation(sprintf("Outgoing %d folders", $changeCount), true); // everything fine, save the sync state for the next time if ($synckey == $newsynckey) { self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $newsyncstate); // update SPA & save it $spa->SetSyncKey($newsynckey); $spa->SetFolderId(false); self::$deviceManager->GetStateManager()->SetSynchedFolderState($spa); // invalidate all pingable flags SyncCollections::InvalidatePingableFlags(); } } self::$encoder->endTag(); return true; }