1<?php 2/*********************************************** 3* File : changesmemorywrapper.php 4* Project : Z-Push 5* Descr : Class that collect changes in memory 6* 7* Created : 18.08.2011 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 ChangesMemoryWrapper extends HierarchyCache implements IImportChanges, IExportChanges { 27 const CHANGE = 1; 28 const DELETION = 2; 29 const SOFTDELETION = 3; 30 const SYNCHRONIZING = 4; 31 32 private $changes; 33 private $step; 34 private $destinationImporter; 35 private $exportImporter; 36 private $impersonating; 37 private $foldersWithoutPermissions; 38 39 /** 40 * Constructor 41 * 42 * @access public 43 * @return 44 */ 45 public function __construct() { 46 $this->changes = array(); 47 $this->step = 0; 48 $this->impersonating = null; 49 $this->foldersWithoutPermissions = array(); 50 parent::__construct(); 51 } 52 53 /** 54 * Only used to load additional folder sync information for hierarchy changes 55 * 56 * @param array $state current state of additional hierarchy folders 57 * 58 * @access public 59 * @return boolean 60 */ 61 public function Config($state, $flags = 0) { 62 if ($this->impersonating == null) { 63 $this->impersonating = (Request::GetImpersonatedUser()) ? strtolower(Request::GetImpersonatedUser()) : false; 64 } 65 66 // we should never forward this changes to a backend 67 if (!isset($this->destinationImporter)) { 68 foreach($state as $addKey => $addFolder) { 69 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : process folder '%s'", $addFolder->displayname)); 70 if (isset($addFolder->NoBackendFolder) && $addFolder->NoBackendFolder == true) { 71 // check rights for readonly access only 72 $hasRights = ZPush::GetBackend()->Setup($addFolder->Store, true, $addFolder->BackendId, true); 73 // delete the folder on the device 74 if (! $hasRights) { 75 // delete the folder only if it was an additional folder before, else ignore it 76 $synchedfolder = $this->GetFolder($addFolder->serverid); 77 if (isset($synchedfolder->NoBackendFolder) && $synchedfolder->NoBackendFolder == true) 78 $this->ImportFolderDeletion($addFolder); 79 continue; 80 } 81 } 82 // make sure, if the folder is already in cache, to set the TypeReal flag (if available) 83 $cacheFolder = $this->GetFolder($addFolder->serverid); 84 if (isset($cacheFolder->TypeReal)) { 85 $addFolder->TypeReal = $cacheFolder->TypeReal; 86 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(): Set REAL foldertype for folder '%s' from cache: '%s'", $addFolder->displayname, $addFolder->TypeReal)); 87 } 88 89 // add folder to the device - if folder is already on the device, nothing will happen 90 $this->ImportFolderChange($addFolder); 91 } 92 93 // look for folders which are currently on the device if there are now not to be synched anymore 94 $alreadyDeleted = $this->GetDeletedFolders(); 95 $folderIdsOnClient = array(); 96 foreach ($this->ExportFolders(true) as $sid => $folder) { 97 // check if previously synchronized secondary contact folders were patched for KOE - if no RealType is set they weren't 98 if ($flags == self::SYNCHRONIZING && ZPush::GetDeviceManager()->IsKoeSupportingSecondaryContacts() && $folder->type == SYNC_FOLDER_TYPE_USER_CONTACT && !isset($folder->TypeReal)) { 99 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(): Identifided secondary contact folder '%s' that was not patched for KOE before. Re-adding it now.", $folder->displayname)); 100 // we need to delete it from the hierarchy cache so it's exported as NEW (way to convince OL to add it) 101 $this->DelFolder($folder->serverid); 102 $folder->flags = SYNC_NEWMESSAGE; 103 $this->ImportFolderChange($folder); 104 } 105 106 // we are only looking at additional folders 107 if (isset($folder->NoBackendFolder)) { 108 // look if this folder is still in the list of additional folders and was not already deleted (e.g. missing permissions) 109 if (!array_key_exists($sid, $state) && !array_key_exists($sid, $alreadyDeleted)) { 110 ZLog::Write(LOGLEVEL_INFO, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : previously synchronized folder '%s' is not to be synched anymore. Sending delete to mobile.", $folder->displayname)); 111 $this->ImportFolderDeletion($folder); 112 } 113 } 114 else { 115 $folderIdsOnClient[] = $sid; 116 } 117 } 118 119 // check permissions on impersonated folders 120 if ($this->impersonating) { 121 ZLog::Write(LOGLEVEL_DEBUG, "ChangesMemoryWrapper->Config(): check permissions of folders of impersonated account"); 122 $hierarchy = ZPush::GetBackend()->GetHierarchy(); 123 foreach ($hierarchy as $folder) { 124 // Check for at least read permissions of the impersonater on folders 125 $hasRights = ZPush::GetBackend()->Setup($this->impersonating, true, $folder->BackendId, true); 126 127 // the folder has no permissions 128 if (!$hasRights) { 129 $this->foldersWithoutPermissions[$folder->serverid] = $folder; 130 // if it's on the device, remove it 131 if (in_array($folder->serverid, $folderIdsOnClient)) { 132 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : previously synchronized folder '%s' has no permissions anymore. Sending delete to mobile.", $folder->displayname)); 133 // delete folder into memory so it's then sent to the client 134 $this->ImportFolderDeletion($folder); 135 } 136 } 137 // has permissions but is not on the device, add it 138 elseif (!in_array($folder->serverid, $folderIdsOnClient)) { 139 $folder->flags = SYNC_NEWMESSAGE; 140 $this->ImportFolderChange($folder); 141 } 142 } 143 } 144 } 145 return true; 146 } 147 148 149 /** 150 * Implement interfaces which are never used 151 */ 152 public function GetState() { return false;} 153 public function LoadConflicts($contentparameters, $state) { return true; } 154 public function ConfigContentParameters($contentparameters) { return true; } 155 public function SetMoveStates($srcState, $dstState = null) { return true; } 156 public function GetMoveStates() { return array(false, false); } 157 public function ImportMessageReadFlag($id, $flags, $categories = array()) { return true; } 158 public function ImportMessageMove($id, $newfolder) { return true; } 159 160 /**---------------------------------------------------------------------------------------------------------- 161 * IImportChanges & destination importer 162 */ 163 164 /** 165 * Sets an importer where incoming changes should be sent to 166 * 167 * @param IImportChanges $importer message to be changed 168 * 169 * @access public 170 * @return boolean 171 */ 172 public function SetDestinationImporter(&$importer) { 173 $this->destinationImporter = $importer; 174 } 175 176 /** 177 * Imports a message change, which is imported into memory 178 * 179 * @param string $id id of message which is changed 180 * @param SyncObject $message message to be changed 181 * 182 * @access public 183 * @return boolean 184 */ 185 public function ImportMessageChange($id, $message) { 186 $this->changes[] = array(self::CHANGE, $id); 187 return true; 188 } 189 190 /** 191 * Imports a message deletion, which is imported into memory 192 * 193 * @param string $id id of message which is deleted 194 * @param boolean $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false 195 * 196 * @access public 197 * @return boolean 198 */ 199 public function ImportMessageDeletion($id, $asSoftDelete = false) { 200 if ($asSoftDelete === true) { 201 $this->changes[] = array(self::SOFTDELETION, $id); 202 } 203 else { 204 $this->changes[] = array(self::DELETION, $id); 205 } 206 return true; 207 } 208 209 /** 210 * Checks if a message id is flagged as changed 211 * 212 * @param string $id message id 213 * 214 * @access public 215 * @return boolean 216 */ 217 public function IsChanged($id) { 218 return (array_search(array(self::CHANGE, $id), $this->changes) === false) ? false:true; 219 } 220 221 /** 222 * Checks if a message id is flagged as deleted 223 * 224 * @param string $id message id 225 * 226 * @access public 227 * @return boolean 228 */ 229 public function IsDeleted($id) { 230 return !((array_search(array(self::DELETION, $id), $this->changes) === false) && (array_search(array(self::SOFTDELETION, $id), $this->changes) === false)); 231 } 232 233 /** 234 * Imports a folder change 235 * 236 * @param SyncFolder $folder folder to be changed 237 * 238 * @access public 239 * @return boolean/SyncObject status/object with the ath least the serverid of the folder set 240 */ 241 public function ImportFolderChange($folder) { 242 // if the destinationImporter is set, then this folder should be processed by another importer 243 // instead of being loaded in memory. 244 if (isset($this->destinationImporter)) { 245 // normally the $folder->type is not set, but we need this value to check if the change operation is permitted 246 // e.g. system folders can normally not be changed - set the type from cache and let the destinationImporter decide 247 if (!isset($folder->type) || ! $folder->type) { 248 $cacheFolder = $this->GetFolder($folder->serverid); 249 $folder->type = $cacheFolder->type; 250 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Set foldertype for folder '%s' from cache as it was not sent: '%s'", $folder->displayname, $folder->type)); 251 if (isset($cacheFolder->TypeReal)) { 252 $folder->TypeReal = $cacheFolder->TypeReal; 253 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Set REAL foldertype for folder '%s' from cache: '%s'", $folder->displayname, $folder->TypeReal)); 254 } 255 } 256 257 // KOE ZO-42: When Notes folders are updated in Outlook, it tries to update the name (that fails by default, as it's a system folder) 258 // catch this case here and ignore the change 259 if (($folder->type == SYNC_FOLDER_TYPE_NOTE || $folder->type == SYNC_FOLDER_TYPE_USER_NOTE) && ZPush::GetDeviceManager()->IsKoe()) { 260 $retFolder = false; 261 } 262 // KOE ZP-907: When a secondary contact folder is patched (update type & change name) don't import it through the backend 263 // This is a bit more permissive than ZPush::GetDeviceManager()->IsKoeSupportingSecondaryContacts() so that updates are always catched 264 // even if the feature was disabled in the meantime. 265 elseif ($folder->type == SYNC_FOLDER_TYPE_UNKNOWN && ZPush::GetDeviceManager()->IsKoe() && !Utils::IsFolderToBeProcessedByKoe($folder)) { 266 ZLog::Write(LOGLEVEL_DEBUG, "ChangesMemoryWrapper->ImportFolderChange(): Rewrote folder type to real type, as KOE patched the folder"); 267 // cacheFolder contains other properties that must be maintained 268 // so we continue using the cacheFolder, but rewrite the type and use the incoming displayname 269 $cacheFolder = $this->GetFolder($folder->serverid); 270 $cacheFolder->type = $cacheFolder->TypeReal; 271 $cacheFolder->displayname = $folder->displayname; 272 $folder = $cacheFolder; 273 $retFolder = $folder; 274 } 275 // do regular folder update 276 else { 277 $retFolder = $this->destinationImporter->ImportFolderChange($folder); 278 } 279 280 // if the operation was sucessfull, update the HierarchyCache 281 if ($retFolder) { 282 // if we get a folder back, we need to update some data in the cache 283 if (isset($retFolder->serverid) && $retFolder->serverid) { 284 // for folder creation, the serverid & backendid are not set and have to be updated 285 if (!isset($folder->serverid) || $folder->serverid == "") { 286 $folder->serverid = $retFolder->serverid; 287 if (isset($retFolder->BackendId) && $retFolder->BackendId) { 288 $folder->BackendId = $retFolder->BackendId; 289 } 290 } 291 292 // if the parentid changed (folder was moved) this needs to be updated as well 293 if ($retFolder->parentid != $folder->parentid) { 294 $folder->parentid = $retFolder->parentid; 295 } 296 } 297 298 $this->AddFolder($folder); 299 } 300 return $retFolder; 301 } 302 // load into memory 303 else { 304 if (isset($folder->serverid)) { 305 // The Zarafa/Kopano HierarchyExporter exports all kinds of changes for folders (e.g. update no. of unread messages in a folder). 306 // These changes are not relevant for the mobiles, as something changes but the relevant displayname and parentid 307 // stay the same. These changes will be dropped and are not sent! 308 if ($folder->equals($this->GetFolder($folder->serverid), false, true)) { 309 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Change for folder '%s' will not be sent as modification is not relevant.", $folder->displayname)); 310 return false; 311 } 312 313 // check if the parent ID is known on the device 314 if (!isset($folder->parentid) || ($folder->parentid != "0" && !$this->GetFolder($folder->parentid))) { 315 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Change for folder '%s' will not be sent as parent folder is not set or not known on mobile.", $folder->displayname)); 316 return false; 317 } 318 319 // ZP-907: if we are ADDING a secondary contact folder and a compatible Outlook is connected, rewrite the type to SYNC_FOLDER_TYPE_UNKNOWN and mark the foldername 320 if (ZPush::GetDeviceManager()->IsKoeSupportingSecondaryContacts() && $folder->type == SYNC_FOLDER_TYPE_USER_CONTACT && 321 (!$this->GetFolder($folder->serverid, true) || !$this->GetFolder($folder->serverid) || $this->GetFolder($folder->serverid)->type === SYNC_FOLDER_TYPE_UNKNOWN) 322 ) { 323 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Sending new folder '%s' as type SYNC_FOLDER_TYPE_UNKNOWN as Outlook is not able to handle secondary contact folders", $folder->displayname)); 324 $folder = Utils::ChangeFolderToTypeUnknownForKoe($folder); 325 } 326 327 // folder changes are only sent if the user has permissions on that folder, if not, change is ignored 328 if ($this->impersonating && array_key_exists($folder->serverid, $this->foldersWithoutPermissions)) { 329 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->ImportFolderChange(): Change for folder '%s' will not be sent as impersonating user has no permissions on folder.", $folder->displayname)); 330 return false; 331 } 332 333 // load this change into memory 334 $this->changes[] = array(self::CHANGE, $folder); 335 336 // HierarchyCache: already add/update the folder so changes are not sent twice (if exported twice) 337 $this->AddFolder($folder); 338 return true; 339 } 340 return false; 341 } 342 } 343 344 /** 345 * Imports a folder deletion 346 * 347 * @param SyncFolder $folder at least "serverid" needs to be set 348 * 349 * @access public 350 * @return boolean 351 */ 352 public function ImportFolderDeletion($folder) { 353 $id = $folder->serverid; 354 355 // if the forwarder is set, then this folder should be processed by another importer 356 // instead of being loaded in mem. 357 if (isset($this->destinationImporter)) { 358 $ret = $this->destinationImporter->ImportFolderDeletion($folder); 359 360 // if the operation was sucessfull, update the HierarchyCache 361 if ($ret) 362 $this->DelFolder($id); 363 364 return $ret; 365 } 366 else { 367 // if this folder is not in the cache, the change does not need to be streamed to the mobile 368 if ($this->GetFolder($id)) { 369 370 // load this change into memory 371 $this->changes[] = array(self::DELETION, $folder); 372 373 // HierarchyCache: delete the folder so changes are not sent twice (if exported twice) 374 $this->DelFolder($id); 375 return true; 376 } 377 } 378 } 379 380 381 /**---------------------------------------------------------------------------------------------------------- 382 * IExportChanges & destination importer 383 */ 384 385 /** 386 * Initializes the Exporter where changes are synchronized to 387 * 388 * @param IImportChanges $importer 389 * 390 * @access public 391 * @return boolean 392 */ 393 public function InitializeExporter(&$importer) { 394 $this->exportImporter = $importer; 395 $this->step = 0; 396 return true; 397 } 398 399 /** 400 * Returns the amount of changes to be exported 401 * 402 * @access public 403 * @return int 404 */ 405 public function GetChangeCount() { 406 return count($this->changes); 407 } 408 409 /** 410 * Synchronizes a change. Only HierarchyChanges will be Synchronized() 411 * 412 * @access public 413 * @return array 414 */ 415 public function Synchronize() { 416 if($this->step < count($this->changes) && isset($this->exportImporter)) { 417 418 $change = $this->changes[$this->step]; 419 420 if ($change[0] == self::CHANGE) { 421 if (! $this->GetFolder($change[1]->serverid, true)) 422 $change[1]->flags = SYNC_NEWMESSAGE; 423 424 $this->exportImporter->ImportFolderChange($change[1]); 425 } 426 // deletion 427 else { 428 $this->exportImporter->ImportFolderDeletion($change[1]); 429 } 430 $this->step++; 431 432 // return progress array 433 return array("steps" => count($this->changes), "progress" => $this->step); 434 } 435 else 436 return false; 437 } 438 439 /** 440 * Initializes a few instance variables 441 * called after unserialization 442 * 443 * @access public 444 * @return array 445 */ 446 public function __wakeup() { 447 $this->changes = array(); 448 $this->step = 0; 449 $this->foldersWithoutPermissions = array(); 450 } 451 452 /** 453 * Removes internal data from the object, so this data can not be exposed. 454 * 455 * @access public 456 * @return boolean 457 */ 458 public function StripData() { 459 unset($this->changes); 460 unset($this->step); 461 unset($this->destinationImporter); 462 unset($this->exportImporter); 463 464 return parent::StripData(); 465 } 466} 467