Commit f692884c authored by Sebastian Kummer's avatar Sebastian Kummer

ZP-556 ZarafaChangesWrapper->init() instantiates a ReplyBackImExporter

if move states exist, on ZarafaChangesWrapper->ImportMessageMove()
checks if the destination folder is writeable and switch to ReplyBack if
not, initialize SyncParameters object of destination folder with zero
synckey if it does not exist (folder is not being synchronized), save
movestates if they changed in MoveItems, include ZarafaChangesWrapper,
ReplyBackImExporter and ReplyBackState in autoloading map,
ReplyBackImExporter tries to undo a MOVE operation for 4 time (deletion
in the destination folder) as OL ignores these requests by default.

Released under the Affero GNU General Public License (AGPL) version 3.
parent 6c2ee8b3
......@@ -45,16 +45,20 @@
************************************************/
class ReplyBackImExporter implements IImportChanges, IExportChanges {
const REPLYBACKID = "ReplyBack";
const EXPORT_DELETE_AFTER_MOVE_TIMES = 3;
const CHANGE = 1;
const DELETION = 2;
const READFLAG = 3;
const CREATION = 4;
const MOVEDHERE = 5;
private $session;
private $store;
private $folderid;
private $changes;
private $changesDest;
private $changesNext;
private $step;
private $exportImporter;
private $mapiprovider;
......@@ -81,6 +85,7 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
$this->step = 0;
$this->changesDest = array();
$this->changesNext = array();
$this->mapiprovider = new MAPIProvider($this->session, $this->store);
$this->moveSrcState = false;
$this->moveDstState = false;
......@@ -126,7 +131,10 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
*/
public function GetState() {
// we can discard all entries in the $changes array up to $step
return array_slice($this->changes, $this->step);
$changes = array_slice($this->changes, $this->step);
$out = array_merge($changes, $this->changesNext);
ZLog::Write(LOGLEVEL_DEBUG, "------- ReplyBack getstate:".print_r($out,1));
return $out;
}
/**
......@@ -140,8 +148,6 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
* @return boolean
*/
public function SetMoveStates($srcState, $dstState = null) {
// TODO remove log
ZLog::Write(LOGLEVEL_DEBUG, "-------------------- ReplyBackImExporter: SetMoveStates: src:". print_r($srcState,1). " dest:". print_r($dstState,1));
if (is_array($srcState)) {
$this->changes = array_merge($this->changes, $srcState);
}
......@@ -161,13 +167,14 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
// if a move was executed, there will be changes for the destination folder, so we have to return the
// source changes as well. If not, they will be transported via GetState().
$srcMoveState = false;
$dstMoveState = $this->changesDest;
if (!empty($this->changesDest)) {
$srcMoveState = $this->changesDest;
$srcMoveState = $this->changes;
}
$ret = array($srcMoveState, $this->changesDest);
// TODO remove log
ZLog::Write(LOGLEVEL_DEBUG, "-------------------- ReplyBackImExporter: GetMoveState: ".print_r($ret,1));
return $ret;
else {
$dstMoveState = false;
}
return array($srcMoveState, $dstMoveState);
}
......@@ -217,8 +224,8 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
$this->changes[] = array(self::DELETION, $id, null);
// generate tmp-id and have it removed later via the dest changes (saved via DstMoveState)
$tmpId = "ReplyBackImExporter-temporaryId-". microtime();
$this->changesDest[] = array(self::CREATION, $tmpId, null);
$tmpId = $this->getTmpId($newfolder);
$this->changesDest[] = array(self::MOVEDHERE, $tmpId, 0);
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ReplyBackImExporter->ImportMessageMove(): Move forbidden. Restoring message in source folder and added a delete request for the destination folder for the id: %s", $tmpId));
return $tmpId;
......@@ -292,7 +299,7 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
return true;
}
// if there is no $id it means it's a new object. We have to reply back that we accepted it and then delete it.
$id = "ReplyBackImExporter-temporaryId-". microtime();
$id = $this->getTmpId();
$this->changes[] = array(self::CREATION, $id, $message);
return $id;
}
......@@ -374,7 +381,18 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
$id = $change[1];
$oldmessage = $change[2];
if ($change[0] === self::CREATION) {
// MOVEDHERE is an OL hack: export the deletion of the destination folder
// several times, because OL doesn't removes the item the first time
// we generate the same change again for EXPORT_DELETE_AFTER_MOVE_TIMES.
if ($change[0] == self::MOVEDHERE) {
$this->exportImporter->ImportMessageDeletion($id);
if (is_int($oldmessage) && $oldmessage < self::EXPORT_DELETE_AFTER_MOVE_TIMES) {
$change[2]++;
$this->changesNext[] = $change;
}
}
else if ($change[0] === self::CREATION || $this->isTmpId($id)) {
$this->exportImporter->ImportMessageDeletion($id);
}
elseif ($change[0] === self::READFLAG) {
......@@ -407,13 +425,38 @@ class ReplyBackImExporter implements IImportChanges, IExportChanges {
return false;
}
/**
* Generates a temporary id.
*
* @param string $backendfolderid
*
* @access private
* @return string
*/
private function getTmpId($backendfolderid) {
return ZPush::GetDeviceManager()->GetFolderIdForBackendId($backendfolderid) .":". self::REPLYBACKID ."". substr(md5(microtime()), 0, 5);
}
/**
* Checks if an id is a temporary id generated by the ReplyBackImExporter.
*
* @access public
* @return boolean
*/
private function isTmpId($id) {
return !!stripos($id, self::REPLYBACKID);
}
private function getMessage($id, $announceErrors = true) {
if (!$id) {
return false;
}
$message = false;
$sourcekey = hex2bin($id);
$parentsourcekey = $this->folderid;
list($fsk, $sk) = MAPIUtils::SplitMessageId($id);
$sourcekey = hex2bin($sk);
$parentsourcekey = hex2bin(ZPush::GetDeviceManager()->GetBackendIdForFolderId($fsk));
$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey);
if(!$entryid) {
......
......@@ -187,6 +187,11 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
$this->replyback = $this->getReplyBackImExporter();
$this->current = $this->replyback;
}
else if (!empty($this->moveSrcState)) {
ZLog::Write(LOGLEVEL_DEBUG, "ZarafaChangesWrapper->init(): Importer: Move state available. Working with ReplyBackImExporter.");
$this->replyback = $this->getReplyBackImExporter();
$this->current = $this->replyback;
}
// permissions ok, instanciate an ICS importer
else {
$this->current = new ImportChangesICS($this->session, $this->store, hex2bin($this->folderid));
......@@ -196,7 +201,7 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
else if ($this->preparedAs == self::EXPORTER){
if (!($this->current instanceof ExportChangesICS)) {
// if there was something imported on a read-only folder, we need to reply back the changes
if ($this->replyback || !empty($this->state->GetReplyBackState())) {
if ($this->replyback || !empty($this->state->GetReplyBackState()) || !empty($this->moveSrcState)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaChangesWrapper->init(): Exporter: read-only folder with folderid: '%s'. Working with ReplyBackImExporter.", Utils::PrintAsString($this->folderid)));
if (!$this->replyback) {
$this->replyback = $this->getReplyBackImExporter();
......@@ -242,7 +247,6 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
* @throws StatusException
*/
public function Config($state, $flags = 0) {
ZLog::Write(LOGLEVEL_DEBUG, "-------------------- ZarafaChangesWrapper->Config:". $state);
// if there is an ICS state, it will remain untouched in the ReplyBackState object
$this->state = ReplyBackState::FromState($state);
......@@ -268,7 +272,7 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
* @throws StatusException
*/
public function ConfigContentParameters($contentparameters) {
//$this->init();
$this->init();
return $this->current->ConfigContentParameters($contentparameters);
}
......@@ -300,7 +304,6 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
* @return boolean
*/
public function SetMoveStates($srcState, $dstState = null) {
ZLog::Write(LOGLEVEL_DEBUG, "-------------------- ZarafaChangesWrapper: SetMoveStates: src:". print_r($srcState,1). " dest:". print_r($dstState,1));
$this->moveSrcState = $srcState;
$this->moveDstState = $dstState;
return true;
......@@ -392,16 +395,17 @@ class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
$this->didMove = true;
// Wwhen we setup the $current importer, we didn't know what we needed to do, so we look only at the src folder.
// Now the $newfolder could be read only as well. So we need to check it's permissions and then switch to a ReplyBackImExporter if its r/o.
if (!self::$backend->HasSecretaryACLs($this->store, $this->folderid)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaChangesWrapper->ImportMessageMove(): destination folderid '%s' is missing permissions. Switching to ReplyBackImExporter.", Utils::PrintAsString($this->folderid)));
// save the state
$this->state->SetICSState( $this->current->GetState());
if (!$this->isReplyBackExporter()) {
if (!self::$backend->HasSecretaryACLs($this->store, $newfolder)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaChangesWrapper->ImportMessageMove(): destination folderid '%s' is missing permissions. Switching to ReplyBackImExporter.", Utils::PrintAsString($newfolder)));
$this->replyback = $this->getReplyBackImExporter();
$this->current = $this->replyback;
$this->current->SetMoveStates($this->moveSrcState, $this->moveDstState);
if (isset($this->state)) {
$this->current->Config($this->state->GetReplyBackState());
// TODO: the contentparameters are not available anymore. Do we really need them?
}
}
}
return $this->current->ImportMessageMove($id, $newfolder);
......
......@@ -199,6 +199,16 @@ class StateManager {
return self::BuildStateKey($this->uuid, $this->newStateCounter);
}
/**
* Returns a counter zero SyncKey.
*
* @access public
* @return string
*/
public function GetZeroSyncKey() {
return self::BuildStateKey($this->getNewUuid(), 0);
}
/**
* Gets the state for a specified synckey (uuid + counter)
*
......
......@@ -112,8 +112,10 @@ class MoveItems extends RequestProcessor {
// get saved SyncParameters of the destination folder
$destSpa = self::$deviceManager->GetStateManager()->GetSynchedFolderState($move["dstfldid"]);
if (!$spa->HasSyncKey())
throw new StatusException(sprintf("MoveItems(): Destination folder id '%s' is not fully synchronized. Unable to perform operation.", $move["dstfldid"]), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
if (!$destSpa->HasSyncKey()) {
$destSpa->SetFolderId($move["dstfldid"]);
$destSpa->SetSyncKey(self::$deviceManager->GetStateManager()->GetZeroSyncKey());
}
$importer->SetMoveStates($spa->GetMoveState(), $destSpa->GetMoveState());
$importer->ConfigContentParameters($spa->GetCPO());
......@@ -123,14 +125,12 @@ class MoveItems extends RequestProcessor {
// Get the move states and save them in the SyncParameters of the src and dst folder
list($srcMoveState, $dstMoveState) = $importer->GetMoveStates();
// TODO REMOVE LOG
ZLog::Write(LOGLEVEL_DEBUG, "Importer ---> GET Move state: src: ". print_r($srcMoveState,1) . " dst: ".print_r($dstMoveState,1));
$spa->SetMoveStates($srcMoveState);
$destSpa->SetMoveStates($dstMoveState);
if ($spa->IsDataChanged()) {
if ($spa->GetMoveState() !== $srcMoveState) {
$spa->SetMoveState($srcMoveState);
self::$deviceManager->GetStateManager()->SetSynchedFolderState($spa);
}
if ($destSpa->IsDataChanged()) {
if ($destSpa->GetMoveState() !== $dstMoveState) {
$destSpa->SetMoveState($dstMoveState);
self::$deviceManager->GetStateManager()->SetSynchedFolderState($destSpa);
}
}
......
......@@ -163,10 +163,11 @@ class Sync extends RequestProcessor {
if ($synckey == "0") {
$spa->RemoveSyncKey();
$spa->DelFolderStat();
$spa->SetMoveState(false);
}
else if ($synckey !== false) {
if ($synckey !== $spa->GetSyncKey() && $synckey !== $spa->GetNewSyncKey()) {
ZLog::Write(LOGLEVEL_DEBUG, "HandleSync(): Synckey does not match latest saved for this folder, removing folderstat to force Exporter setup");
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);
......@@ -1061,7 +1062,8 @@ class Sync extends RequestProcessor {
$windowSize = $globallyAvailable;
}
// send <MoreAvailable/> if there are more changes than fit in the folder windowsize
if($changecount > $windowSize) {
// or there is a move state (another sync should be done afterwards)
if($changecount > $windowSize || $spa->GetMoveState() !== false) {
self::$encoder->startTag(SYNC_MOREAVAILABLE, false, true);
$spa->DelFolderStat();
}
......@@ -1160,9 +1162,7 @@ class Sync extends RequestProcessor {
// update the move state (it should be gone now)
list($moveState,) = $exporter->GetMoveStates();
// TODO REMOVE LOG
ZLog::Write(LOGLEVEL_DEBUG, "EXPORTER ---> GET Move state: ". Utils::PrintAsString($moveState));
$spa->SetMoveStates($moveState);
$spa->SetMoveState($moveState);
}
// nothing exported, but possibly imported - get the importer state
......@@ -1265,14 +1265,14 @@ class Sync extends RequestProcessor {
// set the move state so the importer is aware of previous made moves
$this->importer->SetMoveStates($spa->GetMoveState());
ZLog::Write(LOGLEVEL_DEBUG, "-----------after setmove steates");
// if there is a valid state obtained after importing changes in a previous loop, we use that state
if (isset($actiondata["failstate"]) && isset($actiondata["failstate"]["failedsyncstate"])) {
$this->importer->Config($actiondata["failstate"]["failedsyncstate"], $spa->GetConflict());
}
else
$this->importer->Config($sc->GetParameter($spa, "state"), $spa->GetConflict());
ZLog::Write(LOGLEVEL_DEBUG, "-----------after config");
// the CPO is also needed by the importer to check if imported changes are inside the sync window - see ZP-258
$this->importer->ConfigContentParameters($spa->GetCPO());
$this->importer->LoadConflicts($spa->GetCPO(), $sc->GetParameter($spa, "state"));
......
......@@ -86,6 +86,8 @@ return array(
'ProvisioningRequiredException' => $baseDir . '/lib/exceptions/provisioningrequiredexception.php',
'Recurrence' => $baseDir . '/backend/zarafa/mapi/class.recurrence.php',
'ReplaceNullcharFilter' => $baseDir . '/lib/wbxml/replacenullcharfilter.php',
'ReplyBackImExporter' => $baseDir . '/backend/zarafa/replybackimexporter.php',
'ReplyBackState' => $baseDir . '/backend/zarafa/replybackstate.php',
'Request' => $baseDir . '/lib/request/request.php',
'RequestProcessor' => $baseDir . '/lib/request/requestprocessor.php',
'ResolveRecipients' => $baseDir . '/lib/request/resolverecipients.php',
......@@ -162,6 +164,7 @@ return array(
'ZPushAdmin' => $baseDir . '/lib/utils/zpushadmin.php',
'ZPushAutodiscover' => $baseDir . '/autodiscover/autodiscover.php',
'ZPushException' => $baseDir . '/lib/exceptions/zpushexception.php',
'ZarafaChangesWrapper' => $baseDir . '/backend/zarafa/zarafachangeswrapper.php',
'carddav_backend' => $baseDir . '/include/z_carddav.php',
'iCalComponent' => $baseDir . '/include/iCalendar.php',
'iCalProp' => $baseDir . '/include/iCalendar.php',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment