<?php

/*
 * SPDX-License-Identifier: AGPL-3.0-only
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
 *
 * Provides the FOLDERSYNC command
 */

class FolderSync extends RequestProcessor {
	/**
	 * Handles the FolderSync command.
	 *
	 * @param int $commandCode
	 *
	 * @return bool
	 */
	public function Handle($commandCode) {
		// Parse input
		if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) {
			return false;
		}

		if (!self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) {
			return false;
		}

		$synckey = self::$decoder->getElementContent();

		if (!self::$decoder->getElementEndTag()) {
			return false;
		}

		// every FolderSync with SyncKey 0 should return the supported AS version & command headers
		if ($synckey == "0") {
			self::$specialHeaders = [];
			self::$specialHeaders[] = GSync::GetSupportedProtocolVersions();
			self::$specialHeaders[] = GSync::GetSupportedCommands();
		}

		$status = SYNC_FSSTATUS_SUCCESS;
		$newsynckey = $synckey;

		try {
			$syncstate = self::$deviceManager->GetStateManager()->GetSyncState($synckey);

			// We will be saving the sync state under 'newsynckey'
			$newsynckey = self::$deviceManager->GetStateManager()->GetNewSyncKey($synckey);

			// there are no SyncParameters for the hierarchy, but we use it to save the latest synckeys
			$spa = self::$deviceManager->GetStateManager()->GetSynchedFolderState(false);
		}
		catch (StateInvalidException|StateNotFoundException) {
			$status = SYNC_FSSTATUS_SYNCKEYERROR;
		}

		// The ChangesWrapper caches all imports in-memory, so we can send a change count
		// before sending the actual data.
		// the HierarchyCache is notified and the changes from the PIM are transmitted to the actual backend
		$changesMem = self::$deviceManager->GetHierarchyChangesWrapper();

		// the hierarchyCache should now fully be initialized - check for changes in the additional folders
		$changesMem->Config(GSync::GetAdditionalSyncFolders(false), ChangesMemoryWrapper::SYNCHRONIZING);

		// reset to default store in backend
		self::$backend->Setup(false);

		// process incoming changes
		if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) {
			// Ignore <Count> if present
			if (self::$decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) {
				self::$decoder->getElementContent();
				if (!self::$decoder->getElementEndTag()) {
					return false;
				}
			}

			// Process the changes (either <Add>, <Modify>, or <Remove>)
			$element = self::$decoder->getElement();

			if ($element[EN_TYPE] != EN_TYPE_STARTTAG) {
				return false;
			}

			$importer = false;
			WBXMLDecoder::ResetInWhile("folderSyncIncomingChange");
			while (WBXMLDecoder::InWhile("folderSyncIncomingChange")) {
				$folder = new SyncFolder();
				if (!$folder->Decode(self::$decoder)) {
					break;
				}

				// add the backendId to the SyncFolder object
				$folder->BackendId = self::$deviceManager->GetBackendIdForFolderId($folder->serverid);

				try {
					if ($status == SYNC_FSSTATUS_SUCCESS && !$importer) {
						// Configure the backends importer with last state
						$importer = self::$backend->GetImporter();
						$importer->Config($syncstate);
						// the messages from the PIM will be forwarded to the backend
						$changesMem->forwardImporter($importer);
					}

					if ($status == SYNC_FSSTATUS_SUCCESS) {
						switch ($element[EN_TAG]) {
							case SYNC_ADD:
							case SYNC_MODIFY:
								$serverid = $changesMem->ImportFolderChange($folder);
								break;

							case SYNC_REMOVE:
								$serverid = $changesMem->ImportFolderDeletion($folder);
								break;
						}
					}
					else {
						SLog::Write(LOGLEVEL_WARN, sprintf("Request->HandleFolderSync(): ignoring incoming folderchange for folder '%s' as status indicates problem.", $folder->displayname));
						self::$topCollector->AnnounceInformation("Incoming change ignored", true);
					}
				}
				catch (StatusException $stex) {
					$status = $stex->getCode();
				}
			}

			if (!self::$decoder->getElementEndTag()) {
				return false;
			}
		}
		// no incoming changes
		else {
			// check for a potential process loop
			if ($synckey != "0" && self::$deviceManager->IsHierarchyFullResyncRequired()) {
				$status = SYNC_FSSTATUS_SYNCKEYERROR;
				self::$deviceManager->AnnounceProcessStatus(false, $status);
			}
		}

		if (!self::$decoder->getElementEndTag()) {
			return false;
		}

		// We have processed incoming foldersync requests, now send the PIM
		// our changes

		// Output our WBXML reply now
		self::$encoder->StartWBXML();

		self::$encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC);

		if ($status == SYNC_FSSTATUS_SUCCESS) {
			try {
				// do nothing if this is an invalid device id (like the 'validate' Androids internal client sends)
				if (!Request::IsValidDeviceID()) {
					throw new StatusException(sprintf("Request::IsValidDeviceID() indicated that '%s' is not a valid device id", Request::GetDeviceID()), SYNC_FSSTATUS_SERVERERROR);
				}

				// Changes from backend are sent to the MemImporter and processed for the HierarchyCache.
				// The state which is saved is from the backend, as the MemImporter is only a proxy.
				$exporter = self::$backend->GetExporter();

				$exporter->Config($syncstate);
				$exporter->InitializeExporter($changesMem);

				// Stream all changes to the ImportExportChangesMem
				$totalChanges = $exporter->GetChangeCount();
				$exported = 0;
				$partial = false;
				while (is_array($exporter->Synchronize())) {
					++$exported;

					if (time() % 4) {
						self::$topCollector->AnnounceInformation(sprintf("Exported %d from %d folders", $exported, $totalChanges));
					}

					// if partial sync is allowed, stop if this takes too long
					if (USE_PARTIAL_FOLDERSYNC && Request::IsRequestTimeoutReached()) {
						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));
						self::$topCollector->AnnounceInformation(sprintf("Partial export of %d out of %d folders", $exported, $totalChanges), true);
						self::$deviceManager->SetFolderSyncComplete(false);
						$partial = true;

						break;
					}
				}

				// update the foldersync complete flag
				if (USE_PARTIAL_FOLDERSYNC && $partial === false && self::$deviceManager->GetFolderSyncComplete() === false) {
					// say that we are done with partial syncing
					self::$deviceManager->SetFolderSyncComplete(true);
					// reset the loop data to prevent any loop detection to kick in now
					self::$deviceManager->ClearLoopDetectionData(Request::GetAuthUserString(), Request::GetDeviceID());
					SLog::Write(LOGLEVEL_INFO, "Request->HandleFolderSync(): Chunked exporting of folders completed successfully");
				}

				// get the new state from the backend
				$newsyncstate = (isset($exporter)) ? $exporter->GetState() : "";
			}
			catch (StatusException $stex) {
				if ($stex->getCode() == SYNC_FSSTATUS_CODEUNKNOWN) {
					$status = SYNC_FSSTATUS_SYNCKEYERROR;
				}
				else {
					$status = $stex->getCode();
				}
			}
		}

		self::$encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
		self::$encoder->content($status);
		self::$encoder->endTag();

		if ($status == SYNC_FSSTATUS_SUCCESS) {
			self::$encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY);
			$synckey = ($changesMem->IsStateChanged()) ? $newsynckey : $synckey;
			self::$encoder->content($synckey);
			self::$encoder->endTag();

			// Stream folders directly to the PDA
			$streamimporter = new ImportChangesStream(self::$encoder, false);
			$changesMem->InitializeExporter($streamimporter);
			$changeCount = $changesMem->GetChangeCount();

			self::$encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES);

			self::$encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT);
			self::$encoder->content($changeCount);
			self::$encoder->endTag();
			while ($changesMem->Synchronize());

			self::$encoder->endTag();
			self::$topCollector->AnnounceInformation(sprintf("Outgoing %d folders", $changeCount), true);

			if ($changeCount == 0) {
				self::$deviceManager->CheckFolderData();
			}
			// everything fine, save the sync state for the next time
			if ($synckey == $newsynckey) {
				self::$deviceManager->GetStateManager()->SetSyncState($newsynckey, $newsyncstate);

				// update SPA & save it
				$spa->SetSyncKey($newsynckey);
				$spa->SetFolderId(false);

				// invalidate all pingable flags
				SyncCollections::InvalidatePingableFlags();
			}
			// save the SyncParameters if it changed or the reference policy key is not set or different
			if ($spa instanceof SyncParameters && ($spa->IsDataChanged() || !$spa->HasReferencePolicyKey() || GSync::GetProvisioningManager()->ProvisioningRequired($spa->GetReferencePolicyKey(), true, false))) {
				// saves the SPA (while updating the reference policy key)
				$spa->SetLastSynctime(time());
				self::$deviceManager->GetStateManager()->SetSynchedFolderState($spa);
			}
		}

		self::$encoder->endTag();

		return true;
	}
}
