1<?php 2/*********************************************** 3* File : foldersync.php 4* Project : Z-Push 5* Descr : Provides the FOLDERSYNC command 6* 7* Created : 16.02.2012 8* 9* Copyright 2007 - 2016 Zarafa Deutschland GmbH 10* 11* This program is free software: you can redistribute it and/or modify 12* it under the terms of the GNU Affero General Public License, version 3, 13* as published by the Free Software Foundation. 14* 15* This program is distributed in the hope that it will be useful, 16* but WITHOUT ANY WARRANTY; without even the implied warranty of 17* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18* GNU Affero General Public License for more details. 19* 20* You should have received a copy of the GNU Affero General Public License 21* along with this program. If not, see <http://www.gnu.org/licenses/>. 22* 23* Consult LICENSE file for details 24************************************************/ 25 26class FolderSync extends RequestProcessor { 27 28 /** 29 * Handles the FolderSync command 30 * 31 * @param int $commandCode 32 * 33 * @access public 34 * @return boolean 35 */ 36 public function Handle ($commandCode) { 37 // Parse input 38 if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) 39 return false; 40 41 if(!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) 42 return false; 43 44 $synckey = self::$decoder->getElementContent(); 45 46 if(!self::$decoder->getElementEndTag()) 47 return false; 48 49 // every FolderSync with SyncKey 0 should return the supported AS version & command headers 50 if($synckey == "0") { 51 self::$specialHeaders = array(); 52 self::$specialHeaders[] = ZPush::GetSupportedProtocolVersions(); 53 self::$specialHeaders[] = ZPush::GetSupportedCommands(); 54 } 55 56 $status = SYNC_FSSTATUS_SUCCESS; 57 $newsynckey = $synckey; 58 try { 59 $syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey); 60 61 // We will be saving the sync state under 'newsynckey' 62 $newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey); 63 64 // there are no SyncParameters for the hierarchy, but we use it to save the latest synckeys 65 $spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState(false); 66 } 67 catch (StateNotFoundException $snfex) { 68 $status = SYNC_FSSTATUS_SYNCKEYERROR; 69 } 70 catch (StateInvalidException $sive) { 71 $status = SYNC_FSSTATUS_SYNCKEYERROR; 72 } 73 74 // The ChangesWrapper caches all imports in-memory, so we can send a change count 75 // before sending the actual data. 76 // the HierarchyCache is notified and the changes from the PIM are transmitted to the actual backend 77 $changesMem = self::$deviceManager->GetHierarchyChangesWrapper(); 78 79 // the hierarchyCache should now fully be initialized - check for changes in the additional folders 80 $changesMem->Config(ZPush::GetAdditionalSyncFolders(false), ChangesMemoryWrapper::SYNCHRONIZING); 81 82 // reset to default store in backend 83 self::$backend->Setup(false); 84 85 // process incoming changes 86 if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) { 87 // Ignore <Count> if present 88 if(self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) { 89 self::$decoder->getElementContent(); 90 if(!self::$decoder->getElementEndTag()) 91 return false; 92 } 93 94 // Process the changes (either <Add>, <Modify>, or <Remove>) 95 $element = self::$decoder->getElement(); 96 97 if($element[EN_TYPE] != EN_TYPE_STARTTAG) 98 return false; 99 100 $importer = false; 101 WBXMLDecoder::ResetInWhile("folderSyncIncomingChange"); 102 while(WBXMLDecoder::InWhile("folderSyncIncomingChange")) { 103 $folder = new SyncFolder(); 104 if(!$folder->Decode(self::$decoder)) 105 break; 106 107 // add the backendId to the SyncFolder object 108 $folder->BackendId = self::$deviceManager->GetBackendIdForFolderId($folder->serverid); 109 110 try { 111 if ($status == SYNC_FSSTATUS_SUCCESS && !$importer) { 112 // Configure the backends importer with last state 113 $importer = self::$backend->GetImporter(); 114 $importer->Config($syncstate); 115 // the messages from the PIM will be forwarded to the backend 116 $changesMem->forwardImporter($importer); 117 } 118 119 if ($status == SYNC_FSSTATUS_SUCCESS) { 120 switch($element[EN_TAG]) { 121 case SYNC_ADD: 122 case SYNC_MODIFY: 123 $serverid = $changesMem->ImportFolderChange($folder); 124 break; 125 case SYNC_REMOVE: 126 $serverid = $changesMem->ImportFolderDeletion($folder); 127 break; 128 } 129 } 130 else { 131 ZLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): ignoring incoming folderchange for folder '%s' as status indicates problem.", $folder->displayname)); 132 self::$topCollector->AnnounceInformation("Incoming change ignored", true); 133 } 134 } 135 catch (StatusException $stex) { 136 $status = $stex->getCode(); 137 } 138 } 139 140 if(!self::$decoder->getElementEndTag()) 141 return false; 142 } 143 // no incoming changes 144 else { 145 // check for a potential process loop like described in Issue ZP-5 146 if ($synckey != "0" && self::$deviceManager->IsHierarchyFullResyncRequired()) { 147 $status = SYNC_FSSTATUS_SYNCKEYERROR; 148 self::$deviceManager->AnnounceProcessStatus(false, $status); 149 } 150 } 151 152 if(!self::$decoder->getElementEndTag()) 153 return false; 154 155 // We have processed incoming foldersync requests, now send the PIM 156 // our changes 157 158 // Output our WBXML reply now 159 self::$encoder->StartWBXML(); 160 161 self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC); 162 { 163 if ($status == SYNC_FSSTATUS_SUCCESS) { 164 try { 165 // do nothing if this is an invalid device id (like the 'validate' Androids internal client sends) 166 if (!Request::IsValidDeviceID()) 167 throw new StatusException(sprintf("Request::IsValidDeviceID() indicated that '%s' is not a valid device id", Request::GetDeviceID()), SYNC_FSSTATUS_SERVERERROR); 168 169 // Changes from backend are sent to the MemImporter and processed for the HierarchyCache. 170 // The state which is saved is from the backend, as the MemImporter is only a proxy. 171 $exporter = self::$backend->GetExporter(); 172 173 $exporter->Config($syncstate); 174 $exporter->InitializeExporter($changesMem); 175 176 // Stream all changes to the ImportExportChangesMem 177 $totalChanges = $exporter->GetChangeCount(); 178 $exported = 0; 179 $partial = false; 180 while(is_array($exporter->Synchronize())) { 181 $exported++; 182 183 if (time() % 4 ) { 184 self::$topCollector->AnnounceInformation(sprintf("Exported %d from %d folders", $exported, $totalChanges)); 185 } 186 187 // if partial sync is allowed, stop if this takes too long 188 if (USE_PARTIAL_FOLDERSYNC && Request::IsRequestTimeoutReached()) { 189 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)); 190 self::$topCollector->AnnounceInformation(sprintf("Partial export of %d out of %d folders", $exported, $totalChanges), true); 191 self::$deviceManager->SetFolderSyncComplete(false); 192 $partial = true; 193 break; 194 } 195 } 196 197 // update the foldersync complete flag 198 if (USE_PARTIAL_FOLDERSYNC && $partial == false && self::$deviceManager->GetFolderSyncComplete() === false) { 199 // say that we are done with partial synching 200 self::$deviceManager->SetFolderSyncComplete(true); 201 // reset the loop data to prevent any loop detection to kick in now 202 self::$deviceManager->ClearLoopDetectionData(Request::GetAuthUserString(), Request::GetDeviceID()); 203 ZLog::Write(LOGLEVEL_INFO, "Request->HandleFolderSync(): Chunked exporting of folders completed successfully"); 204 } 205 206 // get the new state from the backend 207 $newsyncstate = (isset($exporter))?$exporter->GetState():""; 208 } 209 catch (StatusException $stex) { 210 if ($stex->getCode() == SYNC_FSSTATUS_CODEUNKNOWN) 211 $status = SYNC_FSSTATUS_SYNCKEYERROR; 212 else 213 $status = $stex->getCode(); 214 } 215 } 216 217 self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS); 218 self::$encoder->content($status); 219 self::$encoder->endTag(); 220 221 if ($status == SYNC_FSSTATUS_SUCCESS) { 222 self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY); 223 $synckey = ($changesMem->IsStateChanged()) ? $newsynckey : $synckey; 224 self::$encoder->content($synckey); 225 self::$encoder->endTag(); 226 227 // Stream folders directly to the PDA 228 $streamimporter = new ImportChangesStream(self::$encoder, false); 229 $changesMem->InitializeExporter($streamimporter); 230 $changeCount = $changesMem->GetChangeCount(); 231 232 self::$encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES); 233 { 234 self::$encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT); 235 self::$encoder->content($changeCount); 236 self::$encoder->endTag(); 237 while($changesMem->Synchronize()); 238 } 239 self::$encoder->endTag(); 240 self::$topCollector->AnnounceInformation(sprintf("Outgoing %d folders",$changeCount), true); 241 242 if ($changeCount == 0) { 243 self::$deviceManager->CheckFolderData(); 244 } 245 // everything fine, save the sync state for the next time 246 if ($synckey == $newsynckey) { 247 self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $newsyncstate); 248 249 // update SPA & save it 250 $spa->SetSyncKey($newsynckey); 251 $spa->SetFolderId(false); 252 253 // invalidate all pingable flags 254 SyncCollections::InvalidatePingableFlags(); 255 } 256 // save the SyncParameters if it changed or the reference policy key is not set or different 257 if ($spa->IsDataChanged() || !$spa->HasReferencePolicyKey() || self::$deviceManager->ProvisioningRequired($spa->GetReferencePolicyKey(), true, false)) { 258 // saves the SPA (while updating the reference policy key) 259 $spa->SetLastSynctime(time()); 260 self::$deviceManager->GetStateManager()->SetSynchedFolderState($spa); 261 } 262 263 } 264 } 265 self::$encoder->endTag(); 266 267 return true; 268 } 269} 270