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