/** * 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; }
/** * 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; $this->singleFolder = true; $this->multiFolderInfo = array(); $this->globallyExportedItems = 0; // 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; // if there are already collections in SyncCollections, this is min. the second folder if ($sc->HasCollections()) { $this->singleFolder = 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(); $spa->DelFolderStat(); $spa->SetMoveState(false); } else { if ($synckey !== false) { if ($synckey !== $spa->GetSyncKey() && $synckey !== $spa->GetNewSyncKey() || !!$spa->GetMoveState()) { ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): Synckey does not match latest saved for this folder or there is a move state, removing folderstat to force Exporter setup"); $spa->DelFolderStat(); } $spa->SetSyncKey($synckey); } } } catch (StateInvalidException $stie) { $spa = new SyncParameters(); $status = SYNC_STATUS_INVALIDSYNCKEY; self::$topCollector->AnnounceInformation("State invalid - Resync folder", $this->singleFolder); self::$deviceManager->ForceFolderResync($folderid); $this->saveMultiFolderInfo("exception", "StateInvalidException"); } // update folderid.. this might be a new object $spa->SetFolderId($folderid); $spa->SetBackendFolderId(self::$deviceManager->GetBackendIdForFolderId($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()), $this->singleFolder); } 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(); WBXMLDecoder::ResetInWhile("syncSupported"); while (WBXMLDecoder::InWhile("syncSupported")) { $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 > WINDOW_SIZE_MAX) { $ws = WINDOW_SIZE_MAX; } $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); // save the current filtertype because it might have been changed on the mobile $currentFilterType = $spa->GetFilterType(); while (self::$decoder->getElementStartTag(SYNC_OPTIONS)) { $firstOption = true; WBXMLDecoder::ResetInWhile("syncOptions"); while (WBXMLDecoder::InWhile("syncOptions")) { // 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); // save the current filtertype because it might have been changed on the mobile $currentFilterType = $spa->GetFilterType(); // 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(); // save the current filtertype because it might have been changed on the mobile $currentFilterType = $spa->GetFilterType(); // 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); } // unset filtertype for KOE GAB folder if (KOE_CAPABILITY_GAB && self::$deviceManager->IsKoe() && $spa->GetBackendFolderId() == self::$deviceManager->GetKoeGabBackendFolderId()) { $spa->SetFilterType(SYNC_FILTERTYPE_ALL); ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): KOE GAB folder - setting filter type to unlimited"); } if ($currentFilterType != $spa->GetFilterType()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync(): filter type has changed (old: '%s', new: '%s'), removing folderstat to force Exporter setup", $currentFilterType, $spa->GetFilterType())); $spa->DelFolderStat(); } // 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; WBXMLDecoder::ResetInWhile("syncActions"); while (WBXMLDecoder::InWhile("syncActions")) { // 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 (($el = self::$decoder->getElementStartTag(SYNC_DATA)) && $el[EN_FLAGS] & EN_FLAGS_CONTENT) { $message = ZPush::getSyncObjectFromFolderClass($spa->GetContentClass()); // KOE ZO-42: OL sends Notes as Appointments if ($spa->GetContentClass() == "Notes" && KOE_CAPABILITY_NOTES && self::$deviceManager->IsKoe()) { ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): KOE sends Notes as Appointments, read as SyncAppointment and convert it into a SyncNote object."); $message = new SyncAppointment(); $message->Decode(self::$decoder); $note = new SyncNote(); if (isset($message->asbody)) { $note->asbody = $message->asbody; } if (isset($message->categories)) { $note->categories = $message->categories; } if (isset($message->subject)) { $note->subject = $message->subject; } if (isset($message->dtstamp)) { $note->lastmodified = $message->dtstamp; } // set SyncNote->Color from a color category $note->SetColorFromCategory(); $message = $note; } else { $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("Processed '%d' incoming changes", $nchanges)); if (!$actiondata["fetchids"]) { self::$topCollector->AnnounceInformation(sprintf("%d incoming", $nchanges), $this->singleFolder); $this->saveMultiFolderInfo("incoming", $nchanges); } 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()); ZLog::Write(LOGLEVEL_DEBUG, "Sync(): Global WindowSize requested: " . $sc->GetGlobalWindowSize()); 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, check permissions and only load confirmed states! try { $sc->LoadAllCollections(false, true, true, true, true); } catch (StateInvalidException $siex) { $status = SYNC_STATUS_INVALIDSYNCKEY; self::$topCollector->AnnounceInformation("StateNotFoundException", $this->singleFolder); $this->saveMultiFolderInfo("exeption", "StateNotFoundException"); } catch (StatusException $stex) { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), $this->singleFolder); $this->saveMultiFolderInfo("exeption", "StatusException"); } // update a few values foreach ($sc as $folderid => $spa) { // manually set getchanges parameter for this collection if it is synchronized if ($spa->HasSyncKey()) { $sc->AddParameter($spa, "getchanges", true); // announce WindowSize to DeviceManager self::$deviceManager->SetWindowSize($folderid, $spa->GetWindowSize()); } } if (!$sc->HasCollections()) { $status = SYNC_STATUS_SYNCREQUESTINCOMPLETE; } } else { if (isset($hbinterval)) { // load the hierarchy data - there are no permissions to verify so we just set it to false if (!$sc->LoadCollection(false, true, false)) { $status = SYNC_STATUS_FOLDERHIERARCHYCHANGED; self::$topCollector->AnnounceInformation(sprintf("StatusException code: %d", $status), $this->singleFolder); $this->saveMultiFolderInfo("exeption", "StatusException"); } } } // 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), $this->singleFolder); $this->saveMultiFolderInfo("exeption", "StatusException"); } } // 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)); } } } } } // Start the output ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): Start Output"); // global status // SYNC_COMMONSTATUS_* start with values from 101 if ($status != SYNC_COMMONSTATUS_SUCCESS && ($status == SYNC_STATUS_FOLDERHIERARCHYCHANGED || $status > 100)) { $this->sendStartTags(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->endTag(); // SYNC_SYNCHRONIZE return true; } // Loop through requested 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; $exporter = false; $streamimporter = false; $newFolderStat = false; $setupExporter = true; // 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())) { // no need to run the exporter if the globalwindowsize is already full if ($sc->GetGlobalWindowSize() == $this->globallyExportedItems) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sync(): no exporter setup for '%s' as GlobalWindowSize is full.", $spa->GetFolderId())); $setupExporter = false; } // if the maximum request timeout is reached, stop processing other collections if (Request::IsRequestTimeoutReached()) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("Sync(): no exporter setup for '%s' as request timeout reached, omitting output for collection.", $spa->GetFolderId())); $setupExporter = false; } // compare the folder statistics if the backend supports this if ($setupExporter && self::$backend->HasFolderStats()) { // check if the folder stats changed -> if not, don't setup the exporter, there are no changes! $newFolderStat = self::$backend->GetFolderStat(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()), $spa->GetBackendFolderId()); if ($newFolderStat !== false && !$spa->IsExporterRunRequired($newFolderStat, true)) { $changecount = 0; $setupExporter = false; } } // Do a full Exporter setup if we can't avoid it if ($setupExporter) { //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->GetBackendFolderId()))) { throw new StatusException(sprintf("HandleSync() could not Setup() the backend for folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), SYNC_STATUS_FOLDERHIERARCHYCHANGED); } // Use the state from the importer, as changes may have already happened $exporter = self::$backend->GetExporter($spa->GetBackendFolderId()); if ($exporter === false) { throw new StatusException(sprintf("HandleSync() could not get an exporter for folder id %s/%s", $spa->GetFolderId(), $spa->GetBackendFolderId()), 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->SetMoveStates($spa->GetMoveState()); $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), $this->singleFolder); $this->saveMultiFolderInfo("queued", $changecount); // 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), $this->singleFolder); $this->saveMultiFolderInfo("exception", "StatusException"); } } } } } // Get a new sync key to output to the client if any changes have been send by the mobile or a new synckey is to be sent if (!empty($actiondata["modifyids"]) || !empty($actiondata["clientids"]) || !empty($actiondata["removeids"]) || !$spa->HasSyncKey() && $status == SYNC_STATUS_SUCCESS) { $spa->SetNewSyncKey(self::$deviceManager->GetStateManager()->GetNewSyncKey($spa->GetSyncKey())); } else { // when reaching the global limit for changes of all collections, stop processing other collections (ZP-697) if ($sc->GetGlobalWindowSize() <= $this->globallyExportedItems) { ZLog::Write(LOGLEVEL_DEBUG, "Global WindowSize for amount of exported changes reached, omitting output for collection."); continue; } // get a new synckey if there are changes are we did not reach the limit yet if ($changecount > 0) { $spa->SetNewSyncKey(self::$deviceManager->GetStateManager()->GetNewSyncKey($spa->GetSyncKey())); } } // Fir AS 14.0+ omit output for folder, if there were no incoming or outgoing changes and no Fetch if (Request::GetProtocolVersion() >= 14.0 && !$spa->HasNewSyncKey() && $changecount == 0 && empty($actiondata["fetchids"]) && $status == SYNC_STATUS_SUCCESS && ($newFolderStat === false || !$spa->IsExporterRunRequired($newFolderStat))) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("HandleSync: No changes found for %s folder id '%s'. Omitting output.", $spa->GetContentClass(), $spa->GetFolderId())); continue; } // if there are no other responses sent, we should end with a global status if ($status == SYNC_STATUS_FOLDERHIERARCHYCHANGED && $this->startTagsSent === false) { $this->sendStartTags(); self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($status); self::$encoder->endTag(); self::$encoder->endTag(); // SYNC_SYNCHRONIZE return true; } // there is something to send here, sync folder to output $this->syncFolder($sc, $spa, $exporter, $changecount, $streamimporter, $status, $newFolderStat); // reset status for the next folder $status = SYNC_STATUS_SUCCESS; } // END foreach collection //SYNC_FOLDERS - only if the starttag was sent if ($this->startFolderTagSent) { self::$encoder->endTag(); } //SYNC_SYNCHRONIZE - only if the starttag was sent if ($this->startTagsSent) { self::$encoder->endTag(); } // final top announcement for a multi-folder sync if ($sc->GetCollectionCount() > 1) { self::$topCollector->AnnounceInformation($this->getMultiFolderInfoLine($sc->GetCollectionCount()), true); } 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; 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; }
/** * 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; }