Commit f80e3589 authored by Sebastian Kummer's avatar Sebastian Kummer

ZP-556 General:

- work with read-only flag for static additional folders,
- change IBackend->Setup() to include a read-only flag,
- fixed Setup() read-only flag for default Backend, Combined and
DiffBackend,
- changed ChangesMemoryWrapper for read-only Setup() flag,
- added read-only flag to SyncFolder,
- added comparision method for SyncObjects that returns a human readable
output with data and differences (needs improvement),
- Check status before doing GetImporter() in Sync,
- recheck folderstat after export to catch ReplyBack changes,
- check for backendIds in resync folder of z-push-admin

For Zarafa:
- check for read/secretary permissions in Setup() depending on flag,
- added template for email notification to zarafa config,
- implemented ZarafaChangesWrapper (common wrapper for exporter &
importer) - decides state based what needs to be done,
- implemented ReplyBackImExporter to process changes on read-only
folders and reply back with the original data (overwriting the changes
on the mobiles),
- added simple StateObject to hold ReplyBack states,
- always return the ZarafaChangesWrapper when content im/exporters are
being requested,
- SendMail() should always work on the default store, not the current
store (when sharing folders),
- changed GetFolderStat() to work with ReplyBack cases.

Released under the Affero GNU General Public License (AGPL) version 3.
parent 683a5bc1
......@@ -139,12 +139,13 @@ class BackendCombined extends Backend implements ISearchProvider {
* @param string $store target store, could contain a "domain\user" value
* @param boolean $checkACLonly if set to true, Setup() should just check ACLs
* @param string $folderid if set, only ACLs on this folderid are relevant
* @param boolean $readonly if set, the folder needs at least read permissions
*
* @access public
* @return boolean
*/
public function Setup($store, $checkACLonly = false, $folderid = false) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Setup('%s', '%s', '%s')", $store, Utils::PrintAsString($checkACLonly), $folderid));
public function Setup($store, $checkACLonly = false, $folderid = false, $readonly = false) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("Combined->Setup('%s', '%s', '%s', '%s')", $store, Utils::PrintAsString($checkACLonly), $folderid, Utils::PrintAsString($readonly)));
if(!is_array($this->backends)){
return false;
}
......@@ -153,7 +154,7 @@ class BackendCombined extends Backend implements ISearchProvider {
if(isset($this->config['backends'][$i]['users']) && isset($this->config['backends'][$i]['users'][$store]['username'])){
$u = $this->config['backends'][$i]['users'][$store]['username'];
}
if($this->backends[$i]->Setup($u, $checkACLonly, $folderid) == false){
if($this->backends[$i]->Setup($u, $checkACLonly, $folderid, $readonly) == false){
ZLog::Write(LOGLEVEL_WARN, "Combined->Setup() failed");
return false;
}
......
......@@ -47,3 +47,35 @@
// Defines the server to which we want to connect
define('MAPI_SERVER', 'http://127.0.0.1:236/zarafa');
// Read-Only shared folders
// When trying to write a change on a read-only folder this data is dropped and replaced on the device of the user.
// Enabling the option below, sends an email to the user notifying that this happened (default enabled).
// If this is disabled, the data will be dropped silently and will be lost.
// The template of the email sent can be customized here. The placeholders can also be used in the subject.
define('READ_ONLY_NOTIFY_LOST_DATA', true);
// String to mark the data changed by the user (that he is trying to save)
define('READ_ONLY_NOTIFY_YOURDATA', 'Your data');
// Email template to be sent to the user
define('READ_ONLY_NOTIFY_SUBJECT', "Z-Push: Writing operation not permitted - data reset");
define('READ_ONLY_NOTIFY_BODY', <<<END
Dear **USERFULLNAME**,
on **DATE** at **TIME** you've tried to save a data in the folder '**FOLDERNAME**' on your device '**MOBILETYPE**' ID: '**MOBILEDEVICEID**'.
This operation was not successful, as you lack write access to this folder.
Your data has been dropped and replaced with the original data on your device to ensure data integrity.
Below the data you tried to save. You should seek other means to it (e.g. forward this email to a person with write access to this folder).
**DIFFERENCES**
If you have questions about this email, please contact your e-mail administrator.
Sincerely,
Your Z-Push system
END
);
// Format of the **DATE** and **TIME** placeholders - more information on formats, see http://php.net/manual/en/function.strftime.php
define('READ_ONLY_NOTIFY_DATE_FORMAT', "%d.%m.%Y");
define('READ_ONLY_NOTIFY_TIME_FORMAT', "%H:%M:%S");
<?php
/***********************************************
* File : replybackimexporter.php
* Project : Z-Push
* Descr : This class fullfills the IImportChanges
* and IExportChanges interfaces.
* Messages that are imported are silently
* ignored and then exported again.
*
* Created : 22.04.2016
*
* Copyright 2016 Zarafa Deutschland GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
class ReplyBackImExporter implements IImportChanges, IExportChanges {
const CHANGE = 1;
const DELETION = 2;
const READFLAG = 3;
const CREATION = 4;
private $session;
private $store;
private $folderid;
private $changes;
private $step;
private $exportImporter;
private $mapiprovider;
private $contentparameters;
/**
* Constructor
*
* @param mapisession $session
* @param mapistore $store
* @param string $folderid
*
* @access public
* @throws StatusException
*/
public function ReplyBackImExporter($session, $store, $folderid) {
$this->session = $session;
$this->store = $store;
$this->folderid = $folderid;
$this->changes = array();
$this->step = 0;
$this->mapiprovider = new MAPIProvider($this->session, $this->store);
}
/**
* Initializes the state and flags
*
* @param string $state
* @param int $flags
*
* @access public
* @return boolean status flag
* @throws StatusException
*/
public function Config($state, $flags = 0) {
if (is_array($state)) {
$this->changes = $state;
}
$this->step = 0;
return true;
}
/**
* Configures additional parameters used for content synchronization
*
* @param ContentParameters $contentparameters
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ConfigContentParameters($contentparameters) {
$this->contentparameters = $contentparameters;
return true;
}
/**
* Reads and returns the current state
*
* @access public
* @return string
*/
public function GetState() {
// we can discard all entries in the $changes array up to $step
return array_slice($this->changes, $this->step);
}
/**
* Implement interfaces which are never used
*/
/**
* Loads objects which are expected to be exported with the state
* Before importing/saving the actual message from the mobile, a conflict detection should be done
*
* @param ContentParameters $contentparameters
* @param string $state
*
* @access public
* @return boolean
* @throws StatusException
*/
public function LoadConflicts($contentparameters, $state) {
return true;
}
/**
* Imports a move of a message. This occurs when a user moves an item to another folder
*
* @param string $id
* @param string $newfolder destination folder
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageMove($id, $newfolder) {
return true;
}
/**
* Imports a change on a folder
*
* @param object $folder SyncFolder
*
* @access public
* @return boolean/SyncObject status/object with the ath least the serverid of the folder set
* @throws StatusException
*/
public function ImportFolderChange($folder) {
return false;
}
/**
* Imports a folder deletion
*
* @param SyncFolder $folder at least "serverid" needs to be set
*
* @access public
* @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS
* @throws StatusException
*/
public function ImportFolderDeletion($folder) {
return false;
}
/**----------------------------------------------------------------------------------------------------------
* IImportChanges
*/
/**
* Imports a message change, which is imported into memory
*
* @param string $id id of message which is changed
* @param SyncObject $message message to be changed
*
* @access public
* @return boolean
*/
public function ImportMessageChange($id, $message) {
// data is going to be dropped, inform the user
if (@constant('READ_ONLY_NOTIFY_LOST_DATA')) {
try {
// get the old message - if there is no old message, this is a "create" action
$oldmessage = $this->getMessage($id, false);
if (!$oldmessage instanceof SyncObject) {
$oldmessage = $message;
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ReplyBackImExporter->ImportMessageChange(): Data send from the mobile will be lost. Sending email to user notifying about this."));
$this->sendNotificationEmail($message, $oldmessage);
}
catch (ZPushException $zpe) {
// TODO should we still print the email to the log so the data is not lost at all?
ZLog::Write(LOGLEVEL_ERROR, "ReplyBackImExporter->ImportMessageChange(): exception sending notification email");
}
}
else {
ZLog::Write(LOGLEVEL_INFO, sprintf("ReplyBackImExporter->ImportMessageChange(): Data received from the mobile will be lost. User was *not* informed as configured (see READ_ONLY_NOTIFY_LOST_DATA)."));
}
if ($id) {
$this->changes[] = array(self::CHANGE, $id, $message);
throw new StatusException(sprintf("ReplyBackImExporter->ImportMessageChange('%s','%s'): Read only folder. Data from PIM will be dropped! Server overwrites PIM.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
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();
$this->changes[] = array(self::CREATION, $id, $message);
return $id;
}
/**
* Imports a message deletion, which is imported into memory
*
* @param string $id id of message which is deleted
*
* @access public
* @return boolean
*/
public function ImportMessageDeletion($id) {
$this->changes[] = array(self::DELETION, $id, null);
throw new StatusException(sprintf("ReplyBackImExporter->ImportMessageDeletion('%s'): Read only folder. Data from PIM will be dropped! Server will readd data.", $id), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
return false;
}
/**
* Imports a change in 'read' flag
* This can never conflict
*
* @param string $id
* @param int $flags
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageReadFlag($id, $flags) {
throw new StatusException(sprintf("ReplyBackImExporter->ImportMessageReadFlag('%s','%s'): Read only folder. Data from PIM will be dropped! Server overwrites PIM.", $id, $flags), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
return false;
}
/**----------------------------------------------------------------------------------------------------------
* IExportChanges & destination importer
*/
/**
* Initializes the Exporter where changes are synchronized to
*
* @param IImportChanges $importer
*
* @access public
* @return boolean
*/
public function InitializeExporter(&$importer) {
$this->exportImporter = $importer;
$this->step = 0;
return true;
}
/**
* Returns the amount of changes to be exported
*
* @access public
* @return int
*/
public function GetChangeCount() {
return count($this->changes);
}
/**
* Synchronizes a change. The previously imported messages are now retrieved from the backend
* and sent back to the mobile.
*
* @access public
* @return array
*/
public function Synchronize() {
if($this->step < count($this->changes) && isset($this->exportImporter)) {
$change = $this->changes[$this->step];
$this->step++;
$status = array("steps" => count($this->changes), "progress" => $this->step);
$id = $change[1];
$oldmessage = $change[2];
if ($change[0] === self::CREATION) {
$this->exportImporter->ImportMessageDeletion($id);
}
else {
// get the server side message
$message = $this->getMessage($id);
if (! $message instanceOf SyncObject) {
return $message;
}
if ($change[0] === self::DELETION) {
$message->flags = SYNC_NEWMESSAGE;
}
else {
$message->flags = "";
}
// only reply back on modify
if ($change[1] !== "") {
$this->exportImporter->ImportMessageChange($id, $message);
}
}
// return progress array
return $status;
}
else
return false;
}
private function getMessage($id, $announceErrors = true) {
if (!$id) {
return false;
}
$message = false;
$sourcekey = hex2bin($id);
$parentsourcekey = $this->folderid;
$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey);
if(!$entryid) {
ZLog::Write(LOGLEVEL_INFO, sprintf("ReplyBackImExporter->getMessage(): Couldn't retrieve message from MAPIProvider, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey), bin2hex($entryid)));
if ($announceErrors) {
return $status;
}
else {
return false;
}
}
$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
try {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ReplyBackImExporter->getMessage(): Getting message from MAPIProvider, sourcekey: '%s', parentsourcekey: '%s', entryid: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey), bin2hex($entryid)));
$message = $this->mapiprovider->GetMessage($mapimessage, $this->contentparameters);
}
catch (SyncObjectBrokenException $mbe) {
if ($announceErrors) {
$brokenSO = $mbe->GetSyncObject();
if (!$brokenSO) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("ReplyBackImExporter->getMessage(): Catched SyncObjectBrokenException but broken SyncObject available"));
}
else {
if (!isset($brokenSO->id)) {
$brokenSO->id = "Unknown ID";
ZLog::Write(LOGLEVEL_ERROR, sprintf("ReplyBackImExporter->getMessage(): Catched SyncObjectBrokenException but no ID of object set"));
}
ZPush::GetDeviceManager()->AnnounceIgnoredMessage(false, $brokenSO->id, $brokenSO);
}
// tell MAPI to ignore the message
return $status;
}
}
return $message;
}
/**
* Sends an email notification to the user containing the data the user tried to save.
*
* @param SyncObject $message
* @param SyncObject $oldmessage
* @return void
*/
private function sendNotificationEmail($message, $oldmessage) {
// get email address and full name of the user
$userinfo = ZPush::GetBackend()->GetUserDetails(Request::GetAuthUser());
// get the name of the folder
$foldername = "unknown";
$folderid = bin2hex($this->folderid);
$folders = ZPush::GetAdditionalSyncFolders();
if (isset($folders[$folderid]) && isset($folders[$folderid]->displayname)) {
$foldername = $folders[$folderid]->displayname;
}
// get the differences between the two objects
$data = substr(get_class($oldmessage), 4) . "\r\n";
$dataarray = $message->EvaluateAndCompare($oldmessage, @constant('READ_ONLY_NOTIFY_YOURDATA'));
foreach($dataarray as $key => $value) {
$value = str_replace("\r", "", $value);
$value = str_replace("\n", str_pad("\r\n",25), $value);
$data .= str_pad(ucfirst($key).":", 25) . $value ."\r\n";
}
// build a simple mime message
$toEmail = $userinfo['emailaddress'];
$mail = "From: Z-Push <no-reply>\r\n";
$mail .= "To: $toEmail\r\n";
$mail .= "Content-Type: text/plain; charset=utf-8\r\n";
$mail .= "Subject: ". @constant('READ_ONLY_NOTIFY_SUBJECT'). "\r\n\r\n";
$mail .= @constant('READ_ONLY_NOTIFY_BODY'). "\r\n";
// replace values of template
$mail = str_replace("**USERFULLNAME**", $userinfo['fullname'], $mail);
$mail = str_replace("**DATE**", strftime(@constant('READ_ONLY_NOTIFY_DATE_FORMAT')), $mail);
$mail = str_replace("**TIME**", strftime(@constant('READ_ONLY_NOTIFY_TIME_FORMAT')), $mail);
$mail = str_replace("**FOLDERNAME**", $foldername, $mail);
$mail = str_replace("**MOBILETYPE**", Request::GetDeviceType(), $mail);
$mail = str_replace("**MOBILEDEVICEID**", Request::GetDeviceID(), $mail);
$mail = str_replace("**DIFFERENCES**", $data, $mail);
// user send email to himself
$m = new SyncSendMail();
$m->saveinsent = false;
$m->replacemime = true;
$m->mime = $mail;
ZPush::GetBackend()->SendMail($m);
}
}
<?php
/***********************************************
* File : replybackstate.php
* Project : Z-Push
* Descr : Holds the state of the ReplyBackImExporter
* and also the ICS state to continue on later
*
* Created : 25.04.2016
*
* Copyright 2016 Zarafa Deutschland GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
class ReplyBackState extends StateObject {
protected $unsetdata = array(
'replybackstate' => array(),
'icsstate' => "",
);
static public function FromState($state) {
if (strpos($state, 'ReplyBackState') !== false) {
return unserialize($state);
}
else {
$s = new ReplyBackState();
$s->SetICSState($state);
$s->SetReplyBackState(array());
return $s;
}
}
static public function ToState($state) {
if (!empty($state->GetReplyBackState())) {
return serialize($state);
}
else {
return $state->GetICSState();
}
}
}
\ No newline at end of file
......@@ -73,6 +73,10 @@ include_once('backend/zarafa/mapiphpwrapper.php');
include_once('backend/zarafa/mapistreamwrapper.php');
include_once('backend/zarafa/importer.php');
include_once('backend/zarafa/exporter.php');
include_once('backend/zarafa/replybackimexporter.php');
include_once('backend/zarafa/zarafachangeswrapper.php');
include_once('backend/zarafa/replybackstate.php');
//setlocale to UTF-8 in order to support properties containing Unicode characters
setlocale(LC_CTYPE, "en_US.UTF-8");
......@@ -122,6 +126,7 @@ class BackendZarafa implements IBackend, ISearchProvider {
$this->folderStatCache = array();
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa using PHP-MAPI version: %s", phpversion("mapi")));
ZarafaChangesWrapper::SetBackend($this);
}
/**
......@@ -242,11 +247,12 @@ class BackendZarafa implements IBackend, ISearchProvider {
* @param string $store target store, could contain a "domain\user" value
* @param boolean $checkACLonly if set to true, Setup() should just check ACLs
* @param string $folderid if set, only ACLs on this folderid are relevant
* @param boolean $readonly if set, the folder needs at least read permissions
*
* @access public
* @return boolean
*/
public function Setup($store, $checkACLonly = false, $folderid = false) {
public function Setup($store, $checkACLonly = false, $folderid = false, $readonly = false) {
list($user, $domain) = Utils::SplitDomainUser($store);
if (!isset($this->mainUser))
......@@ -283,10 +289,15 @@ class BackendZarafa implements IBackend, ISearchProvider {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($admin)));
return $admin;
}
// check 'secretary' permissions on this folder
// check permissions on this folder
else {
if (! $readonly) {
$rights = $this->HasSecretaryACLs($userstore, $folderid);
}
else {
$rights = $this->hasSecretaryACLs($userstore, $folderid);
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights)));
$rights = $this->HasReadACLs($userstore, $folderid);
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for '%s' ACLs on '%s' of store '%s': '%s'", ($readonly?'read':'secretary'), $folderid, $user, Utils::PrintAsString($rights)));
return $rights;
}
}
......@@ -382,13 +393,10 @@ class BackendZarafa implements IBackend, ISearchProvider {
public function GetImporter($folderid = false) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid)));
if($folderid !== false) {
// check if the user of the current store has permissions to import to this folderid
if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
return false;
}
$this->importedFolders[$folderid] = $this->store;
return new ImportChangesICS($this->session, $this->store, hex2bin($folderid));
$wrapper = ZarafaChangesWrapper::GetWrapper($this->storeName, $this->session, $this->store, $folderid, $this->storeName == $this->mainUser);
$wrapper->Prepare(ZarafaChangesWrapper::IMPORTER);
return $wrapper;
}
else
return new ImportChangesICS($this->session, $this->store);
......@@ -406,12 +414,9 @@ class BackendZarafa implements IBackend, ISearchProvider {
*/
public function GetExporter($folderid = false) {
if($folderid !== false) {
// check if the user of the current store has permissions to export from this folderid
if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
return false;
}
return new ExportChangesICS($this->session, $this->store, hex2bin($folderid));
$wrapper = ZarafaChangesWrapper::GetWrapper($this->storeName, $this->session, $this->store, $folderid, $this->storeName == $this->mainUser);
$wrapper->Prepare(ZarafaChangesWrapper::EXPORTER);
return $wrapper;
}
else
return new ExportChangesICS($this->session, $this->store);
......@@ -445,14 +450,14 @@ class BackendZarafa implements IBackend, ISearchProvider {
ZLog::Write(LOGLEVEL_WBXML, "RFC822: ". $rfc822line);
$sendMailProps = MAPIMapping::GetSendMailProperties();
$sendMailProps = getPropIdsFromStrings($this->store, $sendMailProps);
$sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps);
// Open the outbox and create the message there
$storeprops = mapi_getprops($this->store, array($sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]));
$storeprops = mapi_getprops($this->defaultstore, array($sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]));
if(isset($storeprops[$sendMailProps["outboxentryid"]]))
$outbox = mapi_msgstore_openentry($this->store, $storeprops[$sendMailProps["outboxentryid"]]);
$outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]);
if(!$outbox)
if(!isset($outbox))
throw new StatusException(sprintf("ZarafaBackend->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR);
$mapimessage = mapi_folder_createmessage($outbox);
......@@ -464,12 +469,12 @@ class BackendZarafa implements IBackend, ISearchProvider {
ZLog::Write(LOGLEVEL_DEBUG, "Use the mapi_inetmapi_imtomapi function");
$ab = mapi_openaddressbook($this->session);
mapi_inetmapi_imtomapi($this->session, $this->store, $ab, $mapimessage, $sm->mime, array());
mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, array());
// Set the appSeqNr so that tracking tab can be updated for meeting request updates
// @see http://jira.zarafa.com/browse/ZP-68
$meetingRequestProps = MAPIMapping::GetMeetingRequestProperties();
$meetingRequestProps = getPropIdsFromStrings($this->store, $meetingRequestProps);
$meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps);
$props = mapi_getprops($mapimessage, array(PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]));
// Convert sent message's body to UTF-8 if it was a HTML message.
......@@ -486,10 +491,10 @@ class BackendZarafa implements IBackend, ISearchProvider {
}
if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) {
// search for calendar items using goid
$mr = new Meetingrequest($this->store, $mapimessage);
$mr = new Meetingrequest($this->defaultstore, $mapimessage);
$appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]);
if (is_array($appointments) && !empty($appointments)) {
$app = mapi_msgstore_openentry($this->store, $appointments[0]);
$app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]);
$appprops = mapi_getprops($app, array($meetingRequestProps["appSeqNr"]));
if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) {
$mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]];
......@@ -1365,6 +1370,12 @@ class BackendZarafa implements IBackend, ISearchProvider {
$user = $this->mainUser;
}
// if there is a ReplyBackImExporter, the exporter needs to run!
$wrapper = ZarafaChangesWrapper::GetWrapper($user, false, null, $folderid, null);
if ($wrapper && $wrapper->HasReplyBackExporter()) {
return "replyback-". time();
}
if (!isset($this->folderStatCache[$user])) {
$this->folderStatCache[$user] = array();
}
......@@ -1520,7 +1531,16 @@ class BackendZarafa implements IBackend, ISearchProvider {
}
}
private function hasSecretaryACLs($store, $folderid) {
/**
* Checks if the logged in user has secretary permissions on a folder.
*
* @param ressource $store
* @param string $folderid
*
* @access public
* @return boolean
*/
public function HasSecretaryACLs($store, $folderid) {
$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
if (!$entryid) return false;
......@@ -1541,6 +1561,30 @@ class BackendZarafa implements IBackend, ISearchProvider {
return false;
}
/**
* Checks if the logged in user has read permissions on a folder.
*
* @param ressource $store
* @param string $folderid
*
* @access public
* @return boolean
*/
public function HasReadACLs($store, $folderid) {
$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
if (!$entryid) return false;
$folder = mapi_msgstore_openentry($store, $entryid);
if (!$folder) return false;
$props = mapi_getprops($folder, array(PR_RIGHTS));
if (isset($props[PR_RIGHTS]) &&
($props[PR_RIGHTS] & ecRightsReadAny) ) {
return true;
}
return false;
}
/**
* The meta function for out of office settings.
*
......
<?php
/***********************************************
* File : zarafachangeswrapper.php
* Project : Z-Push
* Descr : This class fullfills the IImportChanges
* and IExportChanges interfaces.
* It instantiates the ReplyBackImExporter or
* the ICS Importer or Exporter depending on the need.
* The class decides only when the states are set what needs
* to be done. If there are states from the ReplyBackImExporter
* or the user lacks write permissions on the folder a
* ReplyBackImExporter will be initialized, else defauld ICS.
*
* Created : 25.04.2016
*
* Copyright 2016 Zarafa Deutschland GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
class ZarafaChangesWrapper implements IImportChanges, IExportChanges {
const IMPORTER = 1;
const EXPORTER = 2;
// hold a static list of wrappers for stores & folders
static private $wrappers = array();
static private $backend;
private $preparedAs;
private $current;
private $session;
private $store;
private $folderid;
private $replyback;
private $ownFolder;
private $state;
/**
* Sets the backend to be used by the wrappers. This is used to check for permissions.
* The calls made are not part of IBackend, but are implemented by BackendZarafa only.
*
* @param IBackend $backend
*/
static public function SetBackend($backend) {
self::$backend = $backend;
}
/**
* Gets a wrapper for a folder in a store.
* $session needs to be set in order to create a new wrapper. If no session is set and
* the wrapper does not already exist, false is returned.
*
* @param string $storeName
* @param resource $session
* @param resource $store
* @param string $folderid
* @param boolean $ownFolder
*
* @access public
* @return ZarafaChangesWrapper | boolean
*/
static public function GetWrapper($storeName, $session, $store, $folderid, $ownFolder) {
// check early due to the folderstats
if (isset(self::$wrappers[$storeName][$folderid])) {
return self::$wrappers[$storeName][$folderid];
}
if (!isset(self::$wrappers[$storeName])) {
self::$wrappers[$storeName] = array();
}
if (!isset(self::$wrappers[$storeName][$folderid]) && $session) {
self::$wrappers[$storeName][$folderid] = new ZarafaChangesWrapper($session, $store, $folderid, $ownFolder);
}
else {
return false;
}
return self::$wrappers[$storeName][$folderid];
}
/**
* Constructor
*
* @param mapisession $session
* @param mapistore $store
* @param string $folderid
*
* @access public
* @throws StatusException
*/
public function ZarafaChangesWrapper($session, $store, $folderid, $ownFolder) {
$this->preparedAs = null;
$this->session = $session;
$this->store = $store;
$this->folderid = $folderid;
$this->ownFolder = $ownFolder;
$this->replyback = null;
$this->current = null;
$this->state = null;
}
/**
* Indicates if the wrapper is requested to be an exporter or an importer.
*
* @param int $type eiter ZarafaChangesWrapper::IMPORTER or ZarafaChangesWrapper::EXPORTER
*
* @access public
* @return void
*/
public function Prepare($type) {
$this->preparedAs = $type;
}
/**
* Indicates if the wrapper has a ReplyBackImExporter.
*
* @access public
* @return boolean
*/
public function HasReplyBackExporter() {
return !! $this->replyback;
}
/**
* Indicates if the ReplyBackImExporter is the im/exporter currently being wrapped.
*
* @access private
* @return boolean
*/
private function isReplyBackExporter() {
return $this->current == $this->replyback;
}
/**
* Initializes the correct importer, exporter or ReplyBackImExporter.
* The wrapper needs to be prepared as importer or exporter before.
* If the user lacks permissions, a ReplyBackImExporter will be instantiated and used.
*
* @access private
* @return void
* @throws StatusException, FatalNotImplementedException
*/
private function init() {
if ($this->preparedAs == self::IMPORTER) {
if (!($this->current instanceof ImportChangesICS)) {
// check if the user has permissions to import to this folderid
if (!$this->ownFolder && !self::$backend->HasSecretaryACLs($this->store, $this->folderid)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaChangesWrapper->init(): Importer: missing permissions on folderid: '%s'. Working with ReplyBackImExporter.", Utils::PrintAsString($this->folderid)));
$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));
}
}
}
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())) {
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();
}
$this->current = $this->replyback;
}
else {
// check if the user has permissions to export from this folderid
if (!$this->ownFolder && !self::$backend->HasReadACLs($this->store, $this->folderid)) {
throw new StatusException(sprintf("ZarafaChangesWrapper->init(): Exporter: missing read permissions on folderid: '%s'.", Utils::PrintAsString($this->folderid)), SYNC_STATUS_FOLDERHIERARCHYCHANGED);
}
$this->current = new ExportChangesICS($this->session, $this->store, hex2bin($this->folderid));
}
}
}
else {
throw new FatalNotImplementedException("ZarafaChangesWrapper->init(): ZarafaChangesWrapper was not prepared as importer or exporter.");
}
}
/**
* Returns a new ReplyBackImExporter() for the wrapper.
*
* @access private
* @return ReplyBackImExporter
*/
private function getReplyBackImExporter() {
return new ReplyBackImExporter($this->session, $this->store, hex2bin($this->folderid));
}
/**----------------------------------------------------------------------------------------------------------
* IChanges
*/
/**
* Initializes the state and flags
*
* @param string $state
* @param int $flags
*
* @access public
* @return boolean status flag
* @throws StatusException
*/
public function Config($state, $flags = 0) {
// if there is an ICS state, it will remain untouched in the ReplyBackState object
$this->state = ReplyBackState::FromState($state);
$this->init();
if ($this->isReplyBackExporter()) {
return $this->current->Config($this->state->GetReplyBackState(), $flags);
}
else {
return $this->current->Config($this->state->GetICSState(), $flags);
}
}
/**
* Configures additional parameters used for content synchronization
*
* @param ContentParameters $contentparameters
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ConfigContentParameters($contentparameters) {
$this->init();
return $this->current->ConfigContentParameters($contentparameters);
}
/**
* Reads and returns the current state
*
* @access public
* @return string
*/
public function GetState() {
$newState = $this->current->GetState();
if ($this->isReplyBackExporter()) {
$this->state->SetReplyBackState($newState);
}
else {
$this->state->SetICSState($newState);
}
return ReplyBackState::ToState($this->state);
}
/**----------------------------------------------------------------------------------------------------------
* IImportChanges - pass everything directly through to $this->current
*/
/**
* Loads objects which are expected to be exported with the state
* Before importing/saving the actual message from the mobile, a conflict detection should be done
*
* @param ContentParameters $contentparameters
* @param string $state
*
* @access public
* @return boolean
* @throws StatusException
*/
public function LoadConflicts($contentparameters, $state) {
return $this->current->LoadConflicts($contentparameters, $state);
}
/**
* Imports a single message
*
* @param string $id
* @param SyncObject $message
*
* @access public
* @return boolean/string failure / id of message
* @throws StatusException
*/
public function ImportMessageChange($id, $message) {
return $this->current->ImportMessageChange($id, $message);
}
/**
* Imports a deletion. This may conflict if the local object has been modified
*
* @param string $id
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageDeletion($id) {
return $this->current->ImportMessageDeletion($id);
}
/**
* Imports a change in 'read' flag
* This can never conflict
*
* @param string $id
* @param int $flags
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageReadFlag($id, $flags) {
return $this->current->ImportMessageReadFlag($id, $flags);
}
/**
* Imports a move of a message. This occurs when a user moves an item to another folder
*
* @param string $id
* @param string $newfolder destination folder
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageMove($id, $newfolder) {
return $this->current->ImportMessageMove($id, $newfolder);
}
/**
* Implement interfaces which are never used
*/
/**
* Imports a change on a folder
*
* @param object $folder SyncFolder
*
* @access public
* @return boolean/SyncObject status/object with the ath least the serverid of the folder set
* @throws StatusException
*/
public function ImportFolderChange($folder) {
return false;
}
/**
* Imports a folder deletion
*
* @param SyncFolder $folder at least "serverid" needs to be set
*
* @access public
* @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS
* @throws StatusException
*/
public function ImportFolderDeletion($folder) {
return false;
}
/**----------------------------------------------------------------------------------------------------------
* IExportChanges - pass everything directly through to $this->current
*/
/**
* Initializes the Exporter where changes are synchronized to
*
* @param IImportChanges $importer
*
* @access public
* @return boolean
*/
public function InitializeExporter(&$importer) {
return $this->current->InitializeExporter($importer);
}
/**
* Returns the amount of changes to be exported
*
* @access public
* @return int
*/
public function GetChangeCount() {
return $this->current->GetChangeCount();
}
/**
* Synchronizes a change. The previously imported messages are now retrieved from the backend
* and sent back to the mobile.
*
* @access public
* @return array
*/
public function Synchronize() {
return $this->current->Synchronize();
}
}
\ No newline at end of file
......@@ -283,13 +283,21 @@
* SYNC_FOLDER_TYPE_USER_APPOINTMENT
* SYNC_FOLDER_TYPE_USER_TASK
* SYNC_FOLDER_TYPE_USER_MAIL
* SYNC_FOLDER_TYPE_USER_NOTE
* readonly: indicates if the folder should be opened read-only.
* If set to false, full writing permissions are required.
*
* Additional notes:
* - on Zarafa systems use backend/zarafa/listfolders.php script to get a list
* of available folders
*
* - all Z-Push users must have full writing permissions (secretary rights) so
* the configured folders can be synchronized to the mobile
* - all Z-Push users must have at least reading permissions so the configured
* folders can be synchronized to the mobile. Else they are ignored.
*
* - if read-only is set to 'false' only users with full permissions (secretary
* rights) are able to change entries. For all others, the changes will be
* discarted and overwritten with data from the server. Check backend
* compatibility and configuration for this feature.
*
* - this feature is only partly suitable for multi-tenancy environments,
* as ALL users from ALL tenents need access to the configured store & folder.
......@@ -310,6 +318,7 @@
'folderid' => "",
'name' => "Public Contacts",
'type' => SYNC_FOLDER_TYPE_USER_CONTACT,
'readonly' => false,
),
*/
);
......@@ -77,7 +77,7 @@ class ChangesMemoryWrapper extends HierarchyCache implements IImportChanges, IEx
foreach($state as $addKey => $addFolder) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ChangesMemoryWrapper->Config(AdditionalFolders) : process folder '%s'", $addFolder->displayname));
if (isset($addFolder->NoBackendFolder) && $addFolder->NoBackendFolder == true) {
$hasRights = ZPush::GetBackend()->Setup($addFolder->Store, true, $addFolder->BackendId);
$hasRights = ZPush::GetBackend()->Setup($addFolder->Store, true, $addFolder->BackendId, $addFolder->ReadOnly);
// delete the folder on the device
if (! $hasRights) {
// delete the folder only if it was an additional folder before, else ignore it
......
......@@ -369,6 +369,8 @@ class ZPush {
// save store as custom property which is not streamed directly to the device
$folder->NoBackendFolder = true;
$folder->Store = $af['store'];
$folder->ReadOnly = $af['readonly'];
self::$addSyncFolders[$folder->BackendId] = $folder;
}
......
......@@ -326,6 +326,7 @@ define("SYNC_FOLDERHIERARCHY_VERSION","FolderHierarchy:Version");
define("SYNC_FOLDERHIERARCHY_IGNORE_STORE","FolderHierarchy:IgnoreStore");
define("SYNC_FOLDERHIERARCHY_IGNORE_NOBCKENDFLD","FolderHierarchy:IgnoreNoBackendFolder");
define("SYNC_FOLDERHIERARCHY_IGNORE_BACKENDID","FolderHierarchy:IgnoreBackendId");
define("SYNC_FOLDERHIERARCHY_IGNORE_READONLY","FolderHierarchy:IgnoreReadOnly");
// MeetingResponse
define("SYNC_MEETINGRESPONSE_CALENDARID","MeetingResponse:CalendarId");
......
......@@ -105,7 +105,7 @@ abstract class Backend implements IBackend {
* Methods to be implemented
*
* public function Logon($username, $domain, $password);
* public function Setup($store, $checkACLonly = false, $folderid = false);
* public function Setup($store, $checkACLonly = false, $folderid = false, $readonly = false);
* public function Logoff();
* public function GetHierarchy();
* public function GetImporter($folderid = false);
......
......@@ -69,11 +69,12 @@ abstract class BackendDiff extends Backend {
* @param string $store target store, could contain a "domain\user" value
* @param boolean $checkACLonly if set to true, Setup() should just check ACLs
* @param string $folderid if set, only ACLs on this folderid are relevant
* @param boolean $readonly if set, the folder needs at least read permissions
*
* @access public
* @return boolean
*/
public function Setup($store, $checkACLonly = false, $folderid = false) {
public function Setup($store, $checkACLonly = false, $folderid = false, $readonly = false) {
$this->store = $store;
// we don't know if and how diff backends implement the "admin" check, but this will disable it for the webservice
......
......@@ -102,11 +102,12 @@ interface IBackend {
* @param string $store target store, could contain a "domain\user" value
* @param boolean $checkACLonly if set to true, Setup() should just check ACLs
* @param string $folderid if set, only ACLs on this folderid are relevant
* @param boolean $readonly if set, the folder needs at least read permissions
*
* @access public
* @return boolean
*/
public function Setup($store, $checkACLonly = false, $folderid = false);
public function Setup($store, $checkACLonly = false, $folderid = false, $readonly = false);
/**
* Logs off
......
......@@ -1120,8 +1120,16 @@ class Sync extends RequestProcessor {
// changecount is initialized with 'false', so 0 means no changes!
if ($changecount === 0 || ($changecount !== false && $changecount <= $windowSize)) {
self::$deviceManager->SetFolderSyncStatus($spa->GetFolderId(), DeviceManager::FLD_SYNC_COMPLETED);
// we should update the folderstat, but we recheck to see if it changed. If so, it's not updated to force another sync
$newFolderStatAfterExport = self::$backend->GetFolderStat(ZPush::GetAdditionalSyncFolderStore($spa->GetBackendFolderId()), $spa->GetBackendFolderId());
if ($newFolderStat === $newFolderStatAfterExport) {
$this->setFolderStat($spa, $newFolderStat);
}
else {
ZLog::Write(LOGLEVEL_DEBUG, "Sync() Folderstat differs after export, force another exporter run.");
}
}
else
self::$deviceManager->SetFolderSyncStatus($spa->GetFolderId(), DeviceManager::FLD_SYNC_INPROGRESS);
}
......@@ -1226,6 +1234,7 @@ class Sync extends RequestProcessor {
$status = $this->loadStates($sc, $spa, $actiondata, true);
try {
if ($status == SYNC_STATUS_SUCCESS) {
// Configure importer with last state
$this->importer = self::$backend->GetImporter($spa->GetBackendFolderId());
......@@ -1242,13 +1251,13 @@ class Sync extends RequestProcessor {
// 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"));
}
}
catch (StatusException $stex) {
$status = $stex->getCode();
}
$this->importer->LoadConflicts($spa->GetCPO(), $sc->GetParameter($spa, "state"));
return $status;
}
......
......@@ -54,6 +54,7 @@ class SyncFolder extends SyncObject {
public $Store;
public $NoBackendFolder;
public $BackendId;
public $ReadOnly;
function SyncFolder() {
$mapping = array (
......@@ -79,6 +80,10 @@ class SyncFolder extends SyncObject {
SYNC_FOLDERHIERARCHY_IGNORE_BACKENDID => array ( self::STREAMER_VAR => "BackendId",
self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE),
SYNC_FOLDERHIERARCHY_IGNORE_READONLY => array ( self::STREAMER_VAR => "ReadOnly",
self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE),
);
parent::SyncObject($mapping);
......
......@@ -166,6 +166,80 @@ abstract class SyncObject extends Streamer {
return true;
}
/**
* Compares this a SyncObject to another, while printing out all properties and showing where they differ.
*
* @see SyncObject
* @param SyncObject $odo other SyncObject
* @param string $odoName how different data should be named
* @param int $recCount recursion counter
* @return array with one property per line, key being the property instance variable name
*/
public function EvaluateAndCompare($odo, $odoName = "", $keyprefix = "", $recCount = 0) {
if ($odo === false)
return false;
// check objecttype
if (! ($odo instanceof SyncObject)) {
ZLog::Write(LOGLEVEL_DEBUG, "SyncObject->EvaluateAndCompare() the target object is not a SyncObject");
return false;
}
$out = array();
if ($keyprefix)
$keyprefix = $keyprefix . $recCount;
// check for mapped fields
foreach ($this->mapping as $v) {
$val = $v[self::STREAMER_VAR];
// array of values?
if (isset($v[self::STREAMER_ARRAY])) {
// if neither array is created then don't fail the comparison
if (!isset($this->$val) && !isset($odo->$val)) {
continue;
}
else {
// if both arrays exist then seek for differences in the arrays
if (count(array_diff($this->$val, $odo->$val)) + count(array_diff($odo->$val, $this->$val)) > 0) {
$out[$keyprefix.$val] = implode(", ", $this->$val) ." - ". $odoName .": ". implode(", ", $odo->$val);
}
}
}
else {
// if both are not set, don't even bother the output
if (!isset($this->$val) && !isset($odo->$val)) {
continue;
}
// they are both set
else if (isset($this->$val) && isset($odo->$val)) {
//if they are subobjects, compare them recursively
if (isset($v[self::STREAMER_TYPE])) {
if ($this->$val instanceof SyncObject) {
$out += $this->$val->EvaluateAndCompare($odo->$val, $odoName, substr(get_class($this->$val), 4), $recCount++);
}
// if they are streams, compare the streams
else if ($v[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM_ASPLAIN || $v[self::STREAMER_TYPE] == self::STREAMER_TYPE_STREAM_ASBASE64) {
$t = stream_get_contents($this->$val);
$o = stream_get_contents($odo->$val);
$out[$keyprefix.$val] = ($t === $o) ? $t : $t." - ". $odoName .": ".$o;
}
}
// else just compare their values
else {
if($this->$val === $odo->$val) {
$out[$keyprefix.$val] = $this->$val;
}
else {
$out[$keyprefix.$val] = (isset($this->$val) && $this->$val ? $this->$val:"undefined") ." - ". $odoName .": ". (isset($odo->$val) && $odo->$val ? $odo->$val:"undefined");
}
}
}
}
}
return $out;
}
/**
* String representation of the object
*
......
......@@ -498,6 +498,7 @@ class ZPushAdmin {
'syncfolderid' => $device->GetFolderIdForBackendId($fid, false),
'name' => $so->displayname,
'type' => $so->type,
'readonly' => $so->ReadOnly,
'source' => 'static'
);
}
......
......@@ -632,7 +632,7 @@ class ZPushAdminCLI {
$folders = array();
foreach ($device->GetAllFolderIds() as $folderid) {
// if submitting a folderid as type to resync a specific folder.
if ($folderid == $type) {
if ($folderid == $type || $device->GetFolderBackendId($folderid) === $type) {
printf("Found and resynching requested folderid '%s' on device '%s' of user '%s'\n", $folderid, $deviceId, $user);
$folders[] = $folderid;
break;
......
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