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